diff --git a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js index a0be6c70993..5d675fb4851 100644 --- a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js +++ b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js @@ -138,6 +138,10 @@ class HastToProseMirrorConverterState { return this.stack[this.stack.length - 1]; } + get topNode() { + return this.findInStack((item) => item.type === 'node'); + } + /** * Detects if the node stack is empty */ @@ -179,7 +183,7 @@ class HastToProseMirrorConverterState { */ addText(schema, text) { if (!text) return; - const nodes = this.top.content; + const nodes = this.topNode?.content; const last = nodes[nodes.length - 1]; const node = schema.text(text, this.marks); const merged = maybeMerge(last, node); @@ -189,57 +193,92 @@ class HastToProseMirrorConverterState { } else { nodes.push(node); } - - this.closeMarks(); } /** * Adds a mark to the set of marks stored temporarily - * until addText is called. - * @param {*} markType - * @param {*} attrs + * until an inline node is created. + * @param {https://prosemirror.net/docs/ref/#model.MarkType} schemaType Mark schema type + * @param {https://github.com/syntax-tree/hast#nodes} hastNode AST node that the mark is based on + * @param {Object} attrs Mark attributes + * @param {Object} factorySpec Specifications on how th mark should be created */ - openMark(markType, attrs) { - this.marks = markType.create(attrs).addToSet(this.marks); + openMark(schemaType, hastNode, attrs, factorySpec) { + const mark = schemaType.create(attrs); + this.stack.push({ + type: 'mark', + mark, + attrs, + hastNode, + factorySpec, + }); + + this.marks = mark.addToSet(this.marks); } /** - * Empties the temporary Mark set. + * Removes a mark from the list of active marks that + * are applied to inline nodes. */ - closeMarks() { - this.marks = Mark.none; + closeMark() { + const { mark } = this.stack.pop(); + + this.marks = mark.removeFromSet(this.marks); } /** * Adds a node to the stack data structure. * - * @param {Schema.NodeType} type ProseMirror Schema for the node - * @param {HastNode} hastNode Hast node from which the ProseMirror node will be created + * @param {https://prosemirror.net/docs/ref/#model.NodeType} schemaType ProseMirror Schema for the node + * @param {https://github.com/syntax-tree/hast#nodes} hastNode Hast node from which the ProseMirror node will be created * @param {*} attrs Node’s attributes * @param {*} factorySpec The factory spec used to create the node factory */ - openNode(type, hastNode, attrs, factorySpec) { - this.stack.push({ type, attrs, content: [], hastNode, factorySpec }); + openNode(schemaType, hastNode, attrs, factorySpec) { + this.stack.push({ + type: 'node', + schemaType, + attrs, + content: [], + hastNode, + factorySpec, + }); } /** * Removes the top ProseMirror node from the * conversion stack and adds the node to the * previous element. - * @returns */ closeNode() { - const { type, attrs, content } = this.stack.pop(); - const node = type.createAndFill(attrs, content); + const { schemaType, attrs, content, factorySpec } = this.stack.pop(); + const node = + factorySpec.type === 'inline' && this.marks.length + ? schemaType.createAndFill(attrs, content, this.marks) + : schemaType.createAndFill(attrs, content); - if (!node) return null; + if (!node) { + /* + When the node returned by `createAndFill` is null is because the `content` passed as a parameter + doesn’t conform with the document schema. We are handling the most likely scenario here that happens + when a paragraph is inside another paragraph. - if (this.marks.length) { - this.marks = Mark.none; + This scenario happens when the converter encounters a mark wrapping one or more paragraphs. + In this case, the converter will wrap the mark in a paragraph as well because ProseMirror does + not allow marks wrapping block nodes or being direct children of certain nodes like the root nodes + or list items. + */ + if ( + schemaType.name === 'paragraph' && + content.some((child) => child.type.name === 'paragraph') + ) { + this.topNode.content.push(...content); + } + return null; } if (!this.empty) { - this.top.content.push(node); + this.topNode.content.push(node); } return node; @@ -247,9 +286,27 @@ class HastToProseMirrorConverterState { closeUntil(hastNode) { while (hastNode !== this.top?.hastNode) { - this.closeNode(); + if (this.top.type === 'node') { + this.closeNode(); + } else { + this.closeMark(); + } } } + + buildDoc() { + let doc; + + do { + if (this.top.type === 'node') { + doc = this.closeNode(); + } else { + this.closeMark(); + } + } while (!this.empty); + + return doc; + } } /** @@ -276,7 +333,7 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source) }, text: { selector: 'text', - handle: (state, hastNode) => { + handle: (state, hastNode, parent) => { const found = state.findInStack((node) => isFunction(node.factorySpec.processText)); const { value: text } = hastNode; @@ -284,6 +341,7 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source) return; } + state.closeUntil(parent); state.addText(schema, found ? found.factorySpec.processText(text) : text); }, }, @@ -320,11 +378,7 @@ const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source) } else if (factory.type === 'mark') { const markType = schema.marks[proseMirrorName]; factory.handle = (state, hastNode, parent) => { - state.openMark(markType, getAttrs(factory, hastNode, parent, source)); - - if (factory.inlineContent) { - state.addText(schema, hastNode.value); - } + state.openMark(markType, hastNode, getAttrs(factory, hastNode, parent, source), factory); }; } else if (factory.type === 'ignore') { factory.handle = noop; @@ -357,7 +411,7 @@ const findParent = (ancestors, parent) => { return ancestors[ancestors.length - 1]; }; -const calcTextNodePosition = (textNode) => { +const resolveNodePosition = (textNode) => { const { position, value, type } = textNode; if (type !== 'text' || (!position.start && !position.end) || (position.start && position.end)) { @@ -418,7 +472,7 @@ const wrapInlineElements = (nodes, wrappableTags) => const wrapper = { type: 'element', tagName: 'p', - position: calcTextNodePosition(child), + position: resolveNodePosition(child), children: [child], properties: { wrapper: true }, }; @@ -580,11 +634,5 @@ export const createProseMirrorDocFromMdastTree = ({ return factory.skipChildren === true ? SKIP : true; }); - let doc; - - do { - doc = state.closeNode(); - } while (!state.empty); - - return doc; + return state.buildDoc(); }; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index a725ef972d4..21d5decb15b 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -115,34 +115,6 @@ function deferredInitialisation() { ); } - const searchInputBox = document.querySelector('#search'); - if (searchInputBox) { - searchInputBox.addEventListener( - 'focus', - () => { - if (gon.features?.newHeaderSearch) { - import(/* webpackChunkName: 'globalSearch' */ '~/header_search') - .then(async ({ initHeaderSearchApp }) => { - // In case the user started searching before we bootstrapped, let's pass the search along. - const initialSearchValue = searchInputBox.value; - await initHeaderSearchApp(initialSearchValue); - // this is new #search input element. We need to re-find it. - document.querySelector('#search').focus(); - }) - .catch(() => {}); - } else { - import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete') - .then(({ default: initSearchAutocomplete }) => { - const searchDropdown = initSearchAutocomplete(); - searchDropdown.onSearchInputFocus(); - }) - .catch(() => {}); - } - }, - { once: true }, - ); - } - addSelectOnFocusBehaviour('.js-select-on-focus'); const glTooltipDelay = localStorage.getItem('gl-tooltip-delay'); @@ -169,6 +141,36 @@ function deferredInitialisation() { } } +// loading this inside requestIdleCallback is causing issues +// see https://gitlab.com/gitlab-org/gitlab/-/issues/365746 +const searchInputBox = document.querySelector('#search'); +if (searchInputBox) { + searchInputBox.addEventListener( + 'focus', + () => { + if (gon.features?.newHeaderSearch) { + import(/* webpackChunkName: 'globalSearch' */ '~/header_search') + .then(async ({ initHeaderSearchApp }) => { + // In case the user started searching before we bootstrapped, let's pass the search along. + const initialSearchValue = searchInputBox.value; + await initHeaderSearchApp(initialSearchValue); + // this is new #search input element. We need to re-find it. + document.querySelector('#search').focus(); + }) + .catch(() => {}); + } else { + import(/* webpackChunkName: 'globalSearch' */ './search_autocomplete') + .then(({ default: initSearchAutocomplete }) => { + const searchDropdown = initSearchAutocomplete(); + searchDropdown.onSearchInputFocus(); + }) + .catch(() => {}); + } + }, + { once: true }, + ); +} + const $body = $('body'); const $document = $(document); const bootstrapBreakpoint = bp.getBreakpointSize(); diff --git a/app/assets/stylesheets/highlight/themes/monokai.scss b/app/assets/stylesheets/highlight/themes/monokai.scss index c19189ef31b..f83bad152c5 100644 --- a/app/assets/stylesheets/highlight/themes/monokai.scss +++ b/app/assets/stylesheets/highlight/themes/monokai.scss @@ -115,7 +115,7 @@ $monokai-gh: #75715e; @include hljs-override('section', $monokai-gh); @include hljs-override('bullet', $monokai-n); @include hljs-override('subst', $monokai-p); - @include hljs-override('symbol', $monokai-ni); + @include hljs-override('symbol', $monokai-ss); // Line numbers .file-line-num { diff --git a/app/assets/stylesheets/highlight/themes/white.scss b/app/assets/stylesheets/highlight/themes/white.scss index 8698e448c94..b6ca26e9b39 100644 --- a/app/assets/stylesheets/highlight/themes/white.scss +++ b/app/assets/stylesheets/highlight/themes/white.scss @@ -2,6 +2,7 @@ @import '../white_base'; @include conflict-colors('white'); + @include hljs-override('symbol', $white-ss); } :root { diff --git a/app/models/packages/cleanup/policy.rb b/app/models/packages/cleanup/policy.rb index d7df90a4ce0..c0585a52e6e 100644 --- a/app/models/packages/cleanup/policy.rb +++ b/app/models/packages/cleanup/policy.rb @@ -27,6 +27,10 @@ module Packages # fixed cadence of 12 hours self.next_run_at = Time.zone.now + 12.hours end + + def keep_n_duplicated_package_files_disabled? + keep_n_duplicated_package_files == 'all' + end end end end diff --git a/app/services/alert_management/alerts/update_service.rb b/app/services/alert_management/alerts/update_service.rb index 6bdceb0f27b..f273e15b159 100644 --- a/app/services/alert_management/alerts/update_service.rb +++ b/app/services/alert_management/alerts/update_service.rb @@ -12,7 +12,6 @@ module AlertManagement @alert = alert @param_errors = [] @status = params.delete(:status) - @status_change_reason = params.delete(:status_change_reason) super(project: alert.project, current_user: current_user, params: params) end @@ -37,7 +36,7 @@ module AlertManagement private - attr_reader :alert, :param_errors, :status, :status_change_reason + attr_reader :alert, :param_errors, :status def allowed? current_user&.can?(:update_alert_management_alert, alert) @@ -130,37 +129,16 @@ module AlertManagement def handle_status_change add_status_change_system_note resolve_todos if alert.resolved? - sync_to_incident if should_sync_to_incident? end def add_status_change_system_note - SystemNoteService.change_alert_status(alert, current_user, status_change_reason) + SystemNoteService.change_alert_status(alert, current_user) end def resolve_todos todo_service.resolve_todos_for_target(alert, current_user) end - def sync_to_incident - ::Issues::UpdateService.new( - project: project, - current_user: current_user, - params: { - escalation_status: { - status: status, - status_change_reason: " by changing the status of #{alert.to_reference(project)}" - } - } - ).execute(alert.issue) - end - - def should_sync_to_incident? - alert.issue && - alert.issue.supports_escalation? && - alert.issue.escalation_status && - alert.issue.escalation_status.status != alert.status - end - def filter_duplicate # Only need to check if changing to a not-resolved status return if params[:status_event].blank? || params[:status_event] == :resolve diff --git a/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb b/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb index b5f105a6c89..d11492e062a 100644 --- a/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb +++ b/app/services/incident_management/issuable_escalation_statuses/after_update_service.rb @@ -6,7 +6,6 @@ module IncidentManagement def initialize(issuable, current_user, **params) @issuable = issuable @escalation_status = issuable.escalation_status - @alert = issuable.alert_management_alert super(project: issuable.project, current_user: current_user, params: params) end @@ -19,26 +18,13 @@ module IncidentManagement private - attr_reader :issuable, :escalation_status, :alert + attr_reader :issuable, :escalation_status def after_update - sync_status_to_alert add_status_system_note add_timeline_event end - def sync_status_to_alert - return unless alert - return if alert.status == escalation_status.status - - ::AlertManagement::Alerts::UpdateService.new( - alert, - current_user, - status: escalation_status.status_name, - status_change_reason: " by changing the incident status of #{issuable.to_reference(project)}" - ).execute - end - def add_status_system_note return unless escalation_status.status_previously_changed? diff --git a/app/services/incident_management/issuable_escalation_statuses/build_service.rb b/app/services/incident_management/issuable_escalation_statuses/build_service.rb index 9ebcf72a0c9..b3c57da03cb 100644 --- a/app/services/incident_management/issuable_escalation_statuses/build_service.rb +++ b/app/services/incident_management/issuable_escalation_statuses/build_service.rb @@ -5,30 +5,17 @@ module IncidentManagement class BuildService < ::BaseProjectService def initialize(issue) @issue = issue - @alert = issue.alert_management_alert super(project: issue.project) end def execute - return issue.escalation_status if issue.escalation_status - - issue.build_incident_management_issuable_escalation_status(alert_params) + issue.escalation_status || issue.build_incident_management_issuable_escalation_status end private - attr_reader :issue, :alert - - def alert_params - return {} unless alert - - { - status_event: alert.status_event_for(alert.status_name) - } - end + attr_reader :issue end end end - -IncidentManagement::IssuableEscalationStatuses::BuildService.prepend_mod diff --git a/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb b/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb index 1d0504a6e80..58777848151 100644 --- a/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb +++ b/app/services/incident_management/issuable_escalation_statuses/prepare_update_service.rb @@ -5,7 +5,7 @@ module IncidentManagement class PrepareUpdateService < ::BaseProjectService include Gitlab::Utils::StrongMemoize - SUPPORTED_PARAMS = %i[status status_change_reason].freeze + SUPPORTED_PARAMS = %i[status].freeze def initialize(issuable, current_user, params) @issuable = issuable diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 544b2170a02..acd6d45af7a 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -162,8 +162,6 @@ class IssuableBaseService < ::BaseProjectService return unless result.success? && result[:escalation_status].present? - @escalation_status_change_reason = result[:escalation_status].delete(:status_change_reason) - params[:incident_management_issuable_escalation_status_attributes] = result[:escalation_status] end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index d9210169005..afc61eed287 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -199,8 +199,7 @@ module Issues ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new( issue, - current_user, - status_change_reason: @escalation_status_change_reason # Defined in IssuableBaseService before save + current_user ).execute end diff --git a/app/services/packages/cleanup/execute_policy_service.rb b/app/services/packages/cleanup/execute_policy_service.rb new file mode 100644 index 00000000000..b432f6d0acb --- /dev/null +++ b/app/services/packages/cleanup/execute_policy_service.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Packages + module Cleanup + class ExecutePolicyService + include Gitlab::Utils::StrongMemoize + + MAX_EXECUTION_TIME = 250.seconds + + DUPLICATED_FILES_BATCH_SIZE = 10_000 + MARK_PACKAGE_FILES_FOR_DESTRUCTION_SERVICE_BATCH_SIZE = 200 + + def initialize(policy) + @policy = policy + @counts = { + marked_package_files_total_count: 0, + unique_package_id_and_file_name_total_count: 0 + } + end + + def execute + cleanup_duplicated_files + end + + private + + def cleanup_duplicated_files + return if @policy.keep_n_duplicated_package_files_disabled? + + result = installable_package_files.each_batch(of: DUPLICATED_FILES_BATCH_SIZE) do |package_files| + break :timeout if cleanup_duplicated_files_on(package_files) == :timeout + end + + response_success(timeout: result == :timeout) + end + + def cleanup_duplicated_files_on(package_files) + unique_package_id_and_file_name_from(package_files).each do |package_id, file_name| + result = remove_duplicated_files_for(package_id: package_id, file_name: file_name) + @counts[:marked_package_files_total_count] += result.payload[:marked_package_files_count] + @counts[:unique_package_id_and_file_name_total_count] += 1 + + break :timeout unless result.success? + end + end + + def unique_package_id_and_file_name_from(package_files) + # This is a highly custom query for this service, that's why it's not in the model. + # rubocop: disable CodeReuse/ActiveRecord + package_files.group(:package_id, :file_name) + .having("COUNT(*) > #{@policy.keep_n_duplicated_package_files}") + .pluck(:package_id, :file_name) + # rubocop: enable CodeReuse/ActiveRecord + end + + def remove_duplicated_files_for(package_id:, file_name:) + base = ::Packages::PackageFile.for_package_ids(package_id) + .installable + .with_file_name(file_name) + ids_to_keep = base.recent + .limit(@policy.keep_n_duplicated_package_files) + .pluck_primary_key + + duplicated_package_files = base.id_not_in(ids_to_keep) + ::Packages::MarkPackageFilesForDestructionService.new(duplicated_package_files) + .execute(batch_deadline: batch_deadline, batch_size: MARK_PACKAGE_FILES_FOR_DESTRUCTION_SERVICE_BATCH_SIZE) + end + + def project + @policy.project + end + + def installable_package_files + ::Packages::PackageFile.installable + .for_package_ids( + ::Packages::Package.installable + .for_projects(project.id) + ) + end + + def batch_deadline + strong_memoize(:batch_deadline) do + MAX_EXECUTION_TIME.from_now + end + end + + def response_success(timeout:) + ServiceResponse.success( + message: "Packages cleanup policy executed for project #{project.id}", + payload: { + timeout: timeout, + counts: @counts + } + ) + end + end + end +end diff --git a/app/services/packages/mark_package_files_for_destruction_service.rb b/app/services/packages/mark_package_files_for_destruction_service.rb index 3672b44b409..e7fdd88843a 100644 --- a/app/services/packages/mark_package_files_for_destruction_service.rb +++ b/app/services/packages/mark_package_files_for_destruction_service.rb @@ -9,18 +9,41 @@ module Packages @package_files = package_files end - def execute - @package_files.each_batch(of: BATCH_SIZE) do |batched_package_files| - batched_package_files.update_all(status: :pending_destruction) + def execute(batch_deadline: nil, batch_size: BATCH_SIZE) + timeout = false + updates_count = 0 + min_batch_size = [batch_size, BATCH_SIZE].min + + @package_files.each_batch(of: min_batch_size) do |batched_package_files| + if batch_deadline && Time.zone.now > batch_deadline + timeout = true + break + end + + updates_count += batched_package_files.update_all(status: :pending_destruction) end - service_response_success('Package files are now pending destruction') + payload = { marked_package_files_count: updates_count } + + return response_error(payload) if timeout + + response_success(payload) end private - def service_response_success(message) - ServiceResponse.success(message: message) + def response_success(payload) + ServiceResponse.success( + message: 'Package files are now pending destruction', + payload: payload + ) + end + + def response_error(payload) + ServiceResponse.error( + message: 'Timeout while marking package files as pending destruction', + payload: payload + ) end end end diff --git a/config/feature_flags/development/use_keyset_aware_user_search_query.yml b/config/feature_flags/development/use_keyset_aware_user_search_query.yml index aee293c374b..8c2babfd1c7 100644 --- a/config/feature_flags/development/use_keyset_aware_user_search_query.yml +++ b/config/feature_flags/development/use_keyset_aware_user_search_query.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367025 milestone: '15.2' type: development group: group::optimize -default_enabled: false +default_enabled: true diff --git a/db/migrate/20220708142744_add_composite_index_for_protected_environments.rb b/db/migrate/20220708142744_add_composite_index_for_protected_environments.rb new file mode 100644 index 00000000000..ab93f5ca9ca --- /dev/null +++ b/db/migrate/20220708142744_add_composite_index_for_protected_environments.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddCompositeIndexForProtectedEnvironments < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + # skips the `required_` part because index limit is 63 characters + INDEX_NAME = 'index_protected_environments_on_approval_count_and_created_at' + + def up + add_concurrent_index :protected_environments, %i[required_approval_count created_at], name: INDEX_NAME + end + + def down + remove_concurrent_index :protected_environments, %i[required_approval_count created_at], name: INDEX_NAME + end +end diff --git a/db/migrate/20220708142803_add_composite_index_for_protected_environment_approval_rules.rb b/db/migrate/20220708142803_add_composite_index_for_protected_environment_approval_rules.rb new file mode 100644 index 00000000000..6952489588d --- /dev/null +++ b/db/migrate/20220708142803_add_composite_index_for_protected_environment_approval_rules.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddCompositeIndexForProtectedEnvironmentApprovalRules < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + # uses `pe_` instead of `protected_environment_` because index limit is 63 characters + INDEX_NAME = 'index_pe_approval_rules_on_required_approvals_and_created_at' + + def up + add_concurrent_index :protected_environment_approval_rules, %i[required_approvals created_at], name: INDEX_NAME + end + + def down + remove_concurrent_index :protected_environment_approval_rules, %i[required_approvals created_at], name: INDEX_NAME + end +end diff --git a/db/post_migrate/20220617142124_add_index_on_installable_package_files.rb b/db/post_migrate/20220617142124_add_index_on_installable_package_files.rb new file mode 100644 index 00000000000..e74c6c0935e --- /dev/null +++ b/db/post_migrate/20220617142124_add_index_on_installable_package_files.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddIndexOnInstallablePackageFiles < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + INDEX_NAME = 'idx_pkgs_installable_package_files_on_package_id_id_file_name' + # See https://gitlab.com/gitlab-org/gitlab/-/blob/e3ed2c1f65df2e137fc714485d7d42264a137968/app/models/packages/package_file.rb#L16 + DEFAULT_STATUS = 0 + + def up + add_concurrent_index :packages_package_files, + [:package_id, :id, :file_name], + where: "(status = #{DEFAULT_STATUS})", + name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :packages_package_files, INDEX_NAME + end +end diff --git a/db/post_migrate/20220617143228_replace_packages_index_on_project_id_and_status.rb b/db/post_migrate/20220617143228_replace_packages_index_on_project_id_and_status.rb new file mode 100644 index 00000000000..d1e70f04468 --- /dev/null +++ b/db/post_migrate/20220617143228_replace_packages_index_on_project_id_and_status.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class ReplacePackagesIndexOnProjectIdAndStatus < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + NEW_INDEX_NAME = 'index_packages_packages_on_project_id_and_status_and_id' + OLD_INDEX_NAME = 'index_packages_packages_on_project_id_and_status' + + def up + add_concurrent_index :packages_packages, + [:project_id, :status, :id], + name: NEW_INDEX_NAME + remove_concurrent_index_by_name :packages_packages, OLD_INDEX_NAME + end + + def down + add_concurrent_index :packages_packages, + [:project_id, :status], + name: OLD_INDEX_NAME + remove_concurrent_index_by_name :packages_packages, NEW_INDEX_NAME + end +end diff --git a/db/post_migrate/20220629184402_unset_escalation_policies_for_alert_incidents.rb b/db/post_migrate/20220629184402_unset_escalation_policies_for_alert_incidents.rb new file mode 100644 index 00000000000..89adc4b2703 --- /dev/null +++ b/db/post_migrate/20220629184402_unset_escalation_policies_for_alert_incidents.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class UnsetEscalationPoliciesForAlertIncidents < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class EscalationStatus < MigrationRecord + include EachBatch + + self.table_name = 'incident_management_issuable_escalation_statuses' + + scope :having_alert_policy, -> do + joins( + 'INNER JOIN alert_management_alerts ' \ + 'ON alert_management_alerts.issue_id ' \ + '= incident_management_issuable_escalation_statuses.issue_id' + ) + end + end + + def up + EscalationStatus.each_batch do |escalation_statuses| + escalation_statuses + .where.not(policy_id: nil) + .having_alert_policy + .update_all(policy_id: nil, escalations_started_at: nil) + end + end + + def down + # no-op + # + # We cannot retrieve the exact nullified values. We could + # approximately guess what the values are via the alert's + # escalation policy. However, that may not be accurate + # in all cases, as an alert's escalation policy is implictly + # inferred from the project rather than explicit, like an incident. + # So we won't backfill potentially incorrect data. + # + # This data is functionally safe to delete, as the relevant + # fields are read-only, and exclusively informational. + # + # Re-running the migration will have no effect. + end +end diff --git a/db/schema_migrations/20220617142124 b/db/schema_migrations/20220617142124 new file mode 100644 index 00000000000..c8fd06f2c10 --- /dev/null +++ b/db/schema_migrations/20220617142124 @@ -0,0 +1 @@ +668404076e9cfc91817b8ae3ec995a69ec0db283153bbe497a81eb83c2188ceb \ No newline at end of file diff --git a/db/schema_migrations/20220617143228 b/db/schema_migrations/20220617143228 new file mode 100644 index 00000000000..cb4ac555bc3 --- /dev/null +++ b/db/schema_migrations/20220617143228 @@ -0,0 +1 @@ +547fc0071177395133497cbcec9a9d9ed058fe74f632f5e84d9a6416047503f2 \ No newline at end of file diff --git a/db/schema_migrations/20220629184402 b/db/schema_migrations/20220629184402 new file mode 100644 index 00000000000..7e8b0c47bd1 --- /dev/null +++ b/db/schema_migrations/20220629184402 @@ -0,0 +1 @@ +9414b08c3eacadffd8759739da163eb378776d3ecdb06dab7c66e259ff1bed29 \ No newline at end of file diff --git a/db/schema_migrations/20220708142744 b/db/schema_migrations/20220708142744 new file mode 100644 index 00000000000..980c0b43c52 --- /dev/null +++ b/db/schema_migrations/20220708142744 @@ -0,0 +1 @@ +b93ab540270a4b743c12fe5d1d6963cfeb29ee3b0a1e4e012cd4b3d1b3a08cde \ No newline at end of file diff --git a/db/schema_migrations/20220708142803 b/db/schema_migrations/20220708142803 new file mode 100644 index 00000000000..4eb59905dd0 --- /dev/null +++ b/db/schema_migrations/20220708142803 @@ -0,0 +1 @@ +7929540cf382f282f75f2f9c9dd6196d426ed1edb1f6744da1f0a627e7fb0cfc \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9ad896d801c..14b1a1ba862 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -27116,6 +27116,8 @@ CREATE INDEX idx_pkgs_debian_project_distribution_keys_on_distribution_id ON pac CREATE UNIQUE INDEX idx_pkgs_dep_links_on_pkg_id_dependency_id_dependency_type ON packages_dependency_links USING btree (package_id, dependency_id, dependency_type); +CREATE INDEX idx_pkgs_installable_package_files_on_package_id_id_file_name ON packages_package_files USING btree (package_id, id, file_name) WHERE (status = 0); + CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_cloud_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_cloud_last_sync_at, project_id) WHERE (jira_dvcs_cloud_last_sync_at IS NOT NULL); CREATE INDEX idx_proj_feat_usg_on_jira_dvcs_server_last_sync_at_and_proj_id ON project_feature_usages USING btree (jira_dvcs_server_last_sync_at, project_id) WHERE (jira_dvcs_server_last_sync_at IS NOT NULL); @@ -29096,7 +29098,7 @@ CREATE INDEX index_packages_packages_on_project_id_and_created_at ON packages_pa CREATE INDEX index_packages_packages_on_project_id_and_package_type ON packages_packages USING btree (project_id, package_type); -CREATE INDEX index_packages_packages_on_project_id_and_status ON packages_packages USING btree (project_id, status); +CREATE INDEX index_packages_packages_on_project_id_and_status_and_id ON packages_packages USING btree (project_id, status, id); CREATE INDEX index_packages_packages_on_project_id_and_version ON packages_packages USING btree (project_id, version); @@ -29162,6 +29164,8 @@ CREATE INDEX index_path_locks_on_project_id ON path_locks USING btree (project_i CREATE INDEX index_path_locks_on_user_id ON path_locks USING btree (user_id); +CREATE INDEX index_pe_approval_rules_on_required_approvals_and_created_at ON protected_environment_approval_rules USING btree (required_approvals, created_at); + CREATE UNIQUE INDEX index_personal_access_tokens_on_token_digest ON personal_access_tokens USING btree (token_digest); CREATE INDEX index_personal_access_tokens_on_user_id ON personal_access_tokens USING btree (user_id); @@ -29426,6 +29430,8 @@ CREATE INDEX index_protected_environment_deploy_access_levels_on_group_id ON pro CREATE INDEX index_protected_environment_deploy_access_levels_on_user_id ON protected_environment_deploy_access_levels USING btree (user_id); +CREATE INDEX index_protected_environments_on_approval_count_and_created_at ON protected_environments USING btree (required_approval_count, created_at); + CREATE UNIQUE INDEX index_protected_environments_on_group_id_and_name ON protected_environments USING btree (group_id, name) WHERE (group_id IS NOT NULL); CREATE INDEX index_protected_environments_on_project_id ON protected_environments USING btree (project_id); diff --git a/doc/ci/runners/saas/macos_saas_runner.md b/doc/ci/runners/saas/macos_saas_runner.md index 2f61a74a06c..09a0cc975f2 100644 --- a/doc/ci/runners/saas/macos_saas_runner.md +++ b/doc/ci/runners/saas/macos_saas_runner.md @@ -4,9 +4,9 @@ group: Runner info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# SaaS runners on macOS (Limited Availability) **(PREMIUM SAAS)** +# SaaS runners on macOS (Beta) **(PREMIUM SAAS)** -SaaS runners on macOS are in [Limited Availability](../../../policy/alpha-beta-support.md#limited-availability-la) for approved open source programs and customers in Premium and Ultimate plans. +SaaS runners on macOS are in [Beta]](../../../policy/alpha-beta-support.md#beta-features) for approved open source programs and customers in Premium and Ultimate plans. SaaS runners on macOS provide an on-demand macOS build environment integrated with GitLab SaaS [CI/CD](../../../ci/index.md). @@ -22,11 +22,11 @@ Jobs handled by macOS shared runners on GitLab.com **time out after 2 hours**, r ## Access request process -While in limited availability, to run CI jobs on the macOS runners, GitLab SaaS customer namespaces must be explicitly added to the macOS `allow-list`. Customers who participated in the beta have already been added. +While in beta, to run CI jobs on the macOS runners, GitLab SaaS customer namespaces must be explicitly added to the macOS `allow-list`. After you have been added, you can use the macOS runners for any projects in your namespace. -To request access, open a [limited availability access request](https://gitlab.com/gitlab-com/runner-saas-macos-limited-availability/-/issues/new). +To request access, open an [access request](https://gitlab.com/gitlab-com/runner-saas-macos-limited-availability/-/issues/new). The expected turnaround for activation is two business days. ## Quickstart diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md index 9431fce4255..2fcf6e33613 100644 --- a/doc/development/i18n/proofreader.md +++ b/doc/development/i18n/proofreader.md @@ -62,6 +62,7 @@ are very appreciative of the work done by translators and proofreaders! - Michael Hahnle - [GitLab](https://gitlab.com/mhah), [Crowdin](https://crowdin.com/profile/mhah) - Katrin Leinweber - [GitLab](https://gitlab.com/katrinleinweber), [Crowdin](https://crowdin.com/profile/katrinleinweber) - Justman10000 - [GitLab](https://gitlab.com/Justman10000), [Crowdin](https://crowdin.com/profile/Justman10000) + - Vladislav Wanner - [GitLab](https://gitlab.com/RumBugen), [Crowdin](https://crowdin.com/profile/RumBugen) - Greek - Proofreaders needed. - Hebrew diff --git a/doc/development/packages/debian_repository.md b/doc/development/packages/debian_repository.md index 6f65e04878b..a417ced2e65 100644 --- a/doc/development/packages/debian_repository.md +++ b/doc/development/packages/debian_repository.md @@ -110,12 +110,12 @@ sequenceDiagram DebianProjectPackages->>+FindOrCreateIncomingService: Create "incoming" package DebianProjectPackages->>+CreatePackageFileService: Create "unknown" file Note over DebianProjectPackages: If `.changes` file - DebianProjectPackages->>+ProcessChangesWorker: + DebianProjectPackages->>+ProcessChangesWorker: Schedule worker to process the file DebianProjectPackages->>+Client: 202 Created - ProcessChangesWorker->>+ProcessChangesService: - ProcessChangesService->>+ExtractChangesMetadataService: - ExtractChangesMetadataService->>+ExtractMetadataService: - ExtractMetadataService->>+ParseDebian822Service: + ProcessChangesWorker->>+ProcessChangesService: Start service + ProcessChangesService->>+ExtractChangesMetadataService: Extract changesmetadata + ExtractChangesMetadataService->>+ExtractMetadataService: Extract file metadata + ExtractMetadataService->>+ParseDebian822Service: run `dpkg --field` to get control file ExtractMetadataService->>+ExtractDebMetadataService: If .deb or .udeb ExtractDebMetadataService->>+ParseDebian822Service: run `dpkg --field` to get control file ParseDebian822Service-->>-ExtractDebMetadataService: Parse String as Debian RFC822 control data format @@ -125,8 +125,8 @@ sequenceDiagram ExtractMetadataService-->>-ExtractChangesMetadataService: Parse Metadata file ExtractChangesMetadataService-->>-ProcessChangesService: Return list of files and hashes from the .changes file loop process files listed in .changes - ProcessChangesService->>+ExtractMetadataService: - ExtractMetadataService->>+ParseDebian822Service: + ProcessChangesService->>+ExtractMetadataService: Process file + ExtractMetadataService->>+ParseDebian822Service: run `dpkg --field` to get control file ExtractMetadataService->>+ExtractDebMetadataService: If .deb or .udeb ExtractDebMetadataService->>+ParseDebian822Service: run `dpkg --field` to get control file ParseDebian822Service-->>-ExtractDebMetadataService: Parse String as Debian RFC822 control data format @@ -135,8 +135,8 @@ sequenceDiagram ParseDebian822Service-->>-ExtractMetadataService: Parse String as Debian RFC822 control data format ExtractMetadataService-->>-ProcessChangesService: Use parsed metadata to update "unknown" (or known) file end - ProcessChangesService->>+GenerateDistributionWorker: - GenerateDistributionWorker->>+GenerateDistributionService: + ProcessChangesService->>+GenerateDistributionWorker: Find distribution and start service + GenerateDistributionWorker->>+GenerateDistributionService: Generate distribution GenerateDistributionService->>+GenerateDistributionService: generate component files based on new archs and updates from .changes GenerateDistributionService->>+GenerateDistributionKeyService: generate GPG key for distribution GenerateDistributionKeyService-->>-GenerateDistributionService: GPG key diff --git a/doc/operations/incident_management/alerts.md b/doc/operations/incident_management/alerts.md index af42571f82f..a4b34807094 100644 --- a/doc/operations/incident_management/alerts.md +++ b/doc/operations/incident_management/alerts.md @@ -168,8 +168,9 @@ by changing the status. Setting the status to: - **Acknowledged** limits on-call pages based on the project's [escalation policy](escalation_policies.md). - **Triggered** from **Resolved** restarts the alert escalating from the beginning. -For [alerts with an associated incident](alerts.md#create-an-incident-from-an-alert), -updating the alert status also updates the incident status. +In GitLab 15.1 and earlier, updating the status of an [alert with an associated incident](alerts.md#create-an-incident-from-an-alert) +also updates the incident status. In [GitLab 15.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/356057), +the incident status is independent and does not update when the alert status changes. ### Create an incident from an alert diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md index bd7ef113a55..a6324595312 100644 --- a/doc/operations/incident_management/incidents.md +++ b/doc/operations/incident_management/incidents.md @@ -278,8 +278,9 @@ by changing the status. Setting the status to: - **Acknowledged** limits on-call pages based on the selected [escalation policy](#change-escalation-policy). - **Triggered** from **Resolved** restarts the incident escalating from the beginning. -For [incidents created from alerts](alerts.md#create-an-incident-from-an-alert), -updating the incident status also updates the alert status. +In GitLab 15.1 and earlier, updating the status of an [incident created from an alert](alerts.md#create-an-incident-from-an-alert) +also updates the alert status. In [GitLab 15.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/356057), +the alert status is independent and does not update when the incident status changes. ### Change escalation policy **(PREMIUM)** @@ -296,8 +297,9 @@ Selecting an escalation policy updates the incident status to **Triggered** and Deselecting an escalation policy halts escalation. Refer to the [incident status](#change-incident-status) to manage on-call paging once escalation has begun. -For [incidents created from alerts](alerts.md#create-an-incident-from-an-alert), -the incident's escalation policy reflects the alert's escalation policy and cannot be changed. +In GitLab 15.1 and earlier, the escalation policy for [incidents created from alerts](alerts.md#create-an-incident-from-an-alert) +reflects the alert's escalation policy and cannot be changed. In [GitLab 15.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/356057), +the incident escalation policy is independent and can be changed. ### Manage incidents from Slack diff --git a/doc/operations/incident_management/paging.md b/doc/operations/incident_management/paging.md index 936c297e555..ef5f6c81383 100644 --- a/doc/operations/incident_management/paging.md +++ b/doc/operations/incident_management/paging.md @@ -62,4 +62,3 @@ the rule fires. You can respond to a page or stop incident escalations by [unsetting the incident's escalation policy](incidents.md#change-escalation-policy). To avoid duplicate pages, [incidents created from alerts](alerts.md#create-an-incident-from-an-alert) do not support independent escalation. -Instead, the status and escalation policy fields are synced between the alert and the incident. diff --git a/doc/user/project/merge_requests/csv_export.md b/doc/user/project/merge_requests/csv_export.md index aaa9bec945f..2adcc4d4575 100644 --- a/doc/user/project/merge_requests/csv_export.md +++ b/doc/user/project/merge_requests/csv_export.md @@ -10,7 +10,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w Exporting merge requests CSV enables you and your team to export all the data collected from merge requests into a comma-separated values (CSV) file, which stores tabular data in plain text. -To export merge requests to CSV, navigate to your **Merge requests** from the sidebar of a project and select **Export to CSV**. +To export merge requests to CSV, navigate to your **Merge requests** from the sidebar of a project and select **Export as CSV**. ## CSV Output diff --git a/glfm_specification/example_snapshots/html.yml b/glfm_specification/example_snapshots/html.yml index 6a9acdd03a4..8544d1bc172 100644 --- a/glfm_specification/example_snapshots/html.yml +++ b/glfm_specification/example_snapshots/html.yml @@ -1351,8 +1351,8 @@

aaa

wysiwyg: |- -

- aaa

+

+ aaa

04_05__leaf_blocks__fenced_code_blocks__021: canonical: |
aaa
@@ -1711,8 +1711,7 @@
     

foo

wysiwyg: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. +

foo

04_06__leaf_blocks__html_blocks__021: canonical: |

foo

@@ -5381,7 +5380,7 @@ static: |-

(foo)

wysiwyg: |- -

(foo)

+

(foo)

06_05__inlines__emphasis_and_strong_emphasis__044: canonical: |

Gomphocarpus (Gomphocarpus physocarpus, syn. @@ -5390,15 +5389,15 @@

Gomphocarpus (Gomphocarpus physocarpus, syn. Asclepias physocarpa)

wysiwyg: |- -

Gomphocarpus (Gomphocarpus physocarpus, syn. - Asclepias physocarpa)

+

Gomphocarpus (Gomphocarpus physocarpus, syn. + Asclepias physocarpa)

06_05__inlines__emphasis_and_strong_emphasis__045: canonical: |

foo "bar" foo

static: |-

foo "bar" foo

wysiwyg: |- -

foo "bar" foo

+

foo "bar" foo

06_05__inlines__emphasis_and_strong_emphasis__046: canonical: |

foobar

@@ -5426,7 +5425,7 @@ static: |-

(foo)

wysiwyg: |- -

(foo)

+

(foo)

06_05__inlines__emphasis_and_strong_emphasis__050: canonical: |

__foo__bar

@@ -5461,7 +5460,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__055: canonical: |

foo @@ -5478,7 +5477,7 @@ static: |-

foo bar baz

wysiwyg: |- -

foo bar baz

+

foo bar baz

06_05__inlines__emphasis_and_strong_emphasis__057: canonical: |

foo bar baz

@@ -5506,14 +5505,14 @@ static: |-

foo bar baz

wysiwyg: |- -

foo bar baz

+

foo bar baz

06_05__inlines__emphasis_and_strong_emphasis__061: canonical: |

foobarbaz

static: |-

foobarbaz

wysiwyg: |- -

foobarbaz

+

foobarbaz

06_05__inlines__emphasis_and_strong_emphasis__062: canonical: |

foo**bar

@@ -5527,21 +5526,21 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__064: canonical: |

foo bar

static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__065: canonical: |

foobar

static: |-

foobar

wysiwyg: |- -

foobar

+

foobar

06_05__inlines__emphasis_and_strong_emphasis__066: canonical: |

foobarbaz

@@ -5562,7 +5561,7 @@ static: |-

foo bar baz bim bop

wysiwyg: |- -

foo bar baz bim bop

+

foo bar baz bim bop

06_05__inlines__emphasis_and_strong_emphasis__069: canonical: |

foo bar

@@ -5590,7 +5589,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__073: canonical: |

foo @@ -5607,7 +5606,7 @@ static: |-

foo bar baz

wysiwyg: |- -

foo bar baz

+

foo bar baz

06_05__inlines__emphasis_and_strong_emphasis__075: canonical: |

foo bar baz

@@ -5635,28 +5634,28 @@ static: |-

foo bar baz

wysiwyg: |- -

foo bar baz

+

foo bar baz

06_05__inlines__emphasis_and_strong_emphasis__079: canonical: |

foobarbaz

static: |-

foobarbaz

wysiwyg: |- -

foobarbaz

+

foobarbaz

06_05__inlines__emphasis_and_strong_emphasis__080: canonical: |

foo bar

static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__081: canonical: |

foo bar

static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__082: canonical: |

foo bar baz @@ -5665,15 +5664,15 @@

foo bar baz bim bop

wysiwyg: |- -

foo bar baz - bim bop

+

foo bar baz + bim bop

06_05__inlines__emphasis_and_strong_emphasis__083: canonical: |

foo bar

static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_05__inlines__emphasis_and_strong_emphasis__084: canonical: |

__ is not an empty emphasis

@@ -5932,7 +5931,7 @@ static: |-

foo bar *baz bim bam

wysiwyg: |- -

foo bar *baz bim bam

+

foo bar *baz bim bam

06_05__inlines__emphasis_and_strong_emphasis__121: canonical: |

**foo bar baz

@@ -5988,14 +5987,14 @@ static: |-

a *

wysiwyg: |- -

a *

+

a *

06_05__inlines__emphasis_and_strong_emphasis__129: canonical: |

a _

static: |-

a _

wysiwyg: |- -

a _

+

a _

06_05__inlines__emphasis_and_strong_emphasis__130: canonical: |

**ahttp://foo.bar/?q=**

@@ -6267,15 +6266,14 @@ static: |-

link foo bar #

wysiwyg: |- -

link foo bar#

+

link foo bar#

06_07__inlines__links__033: canonical: |

moon

static: |-

moon

wysiwyg: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. +

moon

06_07__inlines__links__034: canonical: |

[foo bar](/uri)

@@ -6289,7 +6287,7 @@ static: |-

[foo [bar baz](/uri)](/uri)

wysiwyg: |- -

[foo [bar baz](/uri)](/uri)

+

[foo [bar baz](/uri)](/uri)

06_07__inlines__links__036: canonical: |

[foo](uri2)

@@ -6366,15 +6364,14 @@ static: |-

link foo bar #

wysiwyg: |- -

link foo bar#

+

link foo bar#

06_07__inlines__links__047: canonical: |

moon

static: |-

moon

wysiwyg: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. +

moon

06_07__inlines__links__048: canonical: |

[foo bar]ref

@@ -6388,7 +6385,7 @@ static: |-

[foo bar baz]ref

wysiwyg: |- -

[foo bar baz]ref

+

[foo bar baz]ref

06_07__inlines__links__050: canonical: |

*foo*

@@ -6553,7 +6550,7 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_07__inlines__links__071: canonical: |

Foo

@@ -6584,14 +6581,14 @@ static: |-

foo bar

wysiwyg: |- -

foo bar

+

foo bar

06_07__inlines__links__075: canonical: |

[foo bar]

static: |-

[foo bar]

wysiwyg: |- -

[foo bar]

+

[foo bar]

06_07__inlines__links__076: canonical: |

[[bar foo

@@ -7301,8 +7298,8 @@

foo
bar

wysiwyg: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. +

foo
+ bar

06_13__inlines__hard_line_breaks__007: canonical: |

foo
@@ -7311,8 +7308,8 @@

foo
bar

wysiwyg: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. +

foo
+ bar

06_13__inlines__hard_line_breaks__008: canonical: |

code span

@@ -7415,11 +7412,11 @@ 07_01__gitlab_specific_markdown__footnotes__001: canonical: "" static: |- -

footnote reference tag 1

+

footnote reference tag 1

    -
  1. -

    footnote text

    +
  2. +

    footnote text

diff --git a/glfm_specification/example_snapshots/prosemirror_json.yml b/glfm_specification/example_snapshots/prosemirror_json.yml index 85d499ef552..f0bc20ca1ed 100644 --- a/glfm_specification/example_snapshots/prosemirror_json.yml +++ b/glfm_specification/example_snapshots/prosemirror_json.yml @@ -2667,11 +2667,6 @@ "content": [ { "type": "text", - "marks": [ - { - "type": "code" - } - ], "text": "\naaa" } ] @@ -3259,8 +3254,28 @@ ] } 04_06__leaf_blocks__html_blocks__020: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "marks": [ + { + "type": "italic" + }, + { + "type": "strike" + } + ], + "text": "foo" + } + ] + } + ] + } 04_06__leaf_blocks__html_blocks__021: |- { "type": "doc", @@ -11691,12 +11706,20 @@ "marks": [ { "type": "bold" + }, + { + "type": "italic" } ], "text": "foo" }, { "type": "text", + "marks": [ + { + "type": "italic" + } + ], "text": ")" } ] @@ -11722,6 +11745,9 @@ { "type": "text", "marks": [ + { + "type": "bold" + }, { "type": "italic" } @@ -11730,11 +11756,19 @@ }, { "type": "text", + "marks": [ + { + "type": "bold" + } + ], "text": ", syn.\n" }, { "type": "text", "marks": [ + { + "type": "bold" + }, { "type": "italic" } @@ -11743,6 +11777,11 @@ }, { "type": "text", + "marks": [ + { + "type": "bold" + } + ], "text": ")" } ] @@ -11768,6 +11807,9 @@ { "type": "text", "marks": [ + { + "type": "bold" + }, { "type": "italic" } @@ -11776,6 +11818,11 @@ }, { "type": "text", + "marks": [ + { + "type": "bold" + } + ], "text": "\" foo" } ] @@ -11857,12 +11904,20 @@ "marks": [ { "type": "bold" + }, + { + "type": "italic" } ], "text": "foo" }, { "type": "text", + "marks": [ + { + "type": "italic" + } + ], "text": ")" } ] @@ -11971,6 +12026,9 @@ "title": null, "canonicalSrc": null } + }, + { + "type": "italic" } ], "text": "bar" @@ -12020,12 +12078,20 @@ "marks": [ { "type": "bold" + }, + { + "type": "italic" } ], "text": "bar" }, { "type": "text", + "marks": [ + { + "type": "italic" + } + ], "text": " baz" } ] @@ -12121,12 +12187,20 @@ "marks": [ { "type": "bold" + }, + { + "type": "italic" } ], "text": "bar" }, { "type": "text", + "marks": [ + { + "type": "italic" + } + ], "text": " baz" } ] @@ -12154,12 +12228,20 @@ "marks": [ { "type": "bold" + }, + { + "type": "italic" } ], "text": "bar" }, { "type": "text", + "marks": [ + { + "type": "italic" + } + ], "text": "baz" } ] @@ -12207,6 +12289,11 @@ }, { "type": "text", + "marks": [ + { + "type": "italic" + } + ], "text": " bar" } ] @@ -12234,6 +12321,9 @@ "marks": [ { "type": "bold" + }, + { + "type": "italic" } ], "text": "bar" @@ -12263,6 +12353,9 @@ "marks": [ { "type": "bold" + }, + { + "type": "italic" } ], "text": "bar" @@ -12351,22 +12444,25 @@ "marks": [ { "type": "bold" + }, + { + "type": "italic" } ], - "text": "bar " + "text": "bar baz" }, { "type": "text", "marks": [ { - "type": "italic" + "type": "bold" } ], - "text": "baz" + "text": " bim" }, { "type": "text", - "text": " bim bop" + "text": " bop" } ] } @@ -12469,6 +12565,9 @@ "title": null, "canonicalSrc": null } + }, + { + "type": "bold" } ], "text": "bar" @@ -12516,6 +12615,9 @@ { "type": "text", "marks": [ + { + "type": "bold" + }, { "type": "italic" } @@ -12524,6 +12626,11 @@ }, { "type": "text", + "marks": [ + { + "type": "bold" + } + ], "text": " baz" } ] @@ -12617,6 +12724,9 @@ { "type": "text", "marks": [ + { + "type": "bold" + }, { "type": "italic" } @@ -12625,6 +12735,11 @@ }, { "type": "text", + "marks": [ + { + "type": "bold" + } + ], "text": " baz" } ] @@ -12650,6 +12765,9 @@ { "type": "text", "marks": [ + { + "type": "bold" + }, { "type": "italic" } @@ -12658,6 +12776,11 @@ }, { "type": "text", + "marks": [ + { + "type": "bold" + } + ], "text": "baz" } ] @@ -12685,6 +12808,11 @@ }, { "type": "text", + "marks": [ + { + "type": "bold" + } + ], "text": " bar" } ] @@ -12710,6 +12838,9 @@ { "type": "text", "marks": [ + { + "type": "bold" + }, { "type": "italic" } @@ -12739,24 +12870,27 @@ { "type": "text", "marks": [ + { + "type": "bold" + }, { "type": "italic" } ], - "text": "bar " + "text": "bar baz" }, { "type": "text", "marks": [ { - "type": "bold" + "type": "italic" } ], - "text": "baz" + "text": "\nbim" }, { "type": "text", - "text": "\nbim bop" + "text": " bop" } ] } @@ -12791,6 +12925,9 @@ "canonicalSrc": null } }, + { + "type": "bold" + }, { "type": "italic" } @@ -13602,12 +13739,20 @@ "marks": [ { "type": "bold" + }, + { + "type": "italic" } ], "text": "bar *baz bim" }, { "type": "text", + "marks": [ + { + "type": "italic" + } + ], "text": " bam" } ] @@ -13798,6 +13943,9 @@ { "type": "text", "marks": [ + { + "type": "italic" + }, { "type": "code" } @@ -13827,6 +13975,9 @@ { "type": "text", "marks": [ + { + "type": "italic" + }, { "type": "code" } @@ -14769,6 +14920,16 @@ { "type": "text", "marks": [ + { + "type": "link", + "attrs": { + "href": "/uri", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": null + } + }, { "type": "italic" } @@ -14778,8 +14939,21 @@ { "type": "text", "marks": [ + { + "type": "link", + "attrs": { + "href": "/uri", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": null + } + }, { "type": "bold" + }, + { + "type": "italic" } ], "text": "bar" @@ -14787,6 +14961,22 @@ { "type": "text", "marks": [ + { + "type": "link", + "attrs": { + "href": "/uri", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": null + } + }, + { + "type": "bold" + }, + { + "type": "italic" + }, { "type": "code" } @@ -14798,8 +14988,38 @@ ] } 06_07__inlines__links__033: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "image", + "attrs": { + "src": "moon.jpg", + "alt": "moon", + "title": null, + "uploading": false, + "canonicalSrc": null + }, + "marks": [ + { + "type": "link", + "attrs": { + "href": "/uri", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": null + } + } + ] + } + ] + } + ] + } 06_07__inlines__links__034: |- { "type": "doc", @@ -14867,13 +15087,25 @@ "title": null, "canonicalSrc": null } + }, + { + "type": "italic" } ], "text": "baz" }, { "type": "text", - "text": "](/uri)](/uri)" + "marks": [ + { + "type": "italic" + } + ], + "text": "](/uri)" + }, + { + "type": "text", + "text": "](/uri)" } ] } @@ -15159,6 +15391,16 @@ { "type": "text", "marks": [ + { + "type": "link", + "attrs": { + "href": "/uri", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": null + } + }, { "type": "italic" } @@ -15168,8 +15410,21 @@ { "type": "text", "marks": [ + { + "type": "link", + "attrs": { + "href": "/uri", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": null + } + }, { "type": "bold" + }, + { + "type": "italic" } ], "text": "bar" @@ -15177,6 +15432,22 @@ { "type": "text", "marks": [ + { + "type": "link", + "attrs": { + "href": "/uri", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": null + } + }, + { + "type": "bold" + }, + { + "type": "italic" + }, { "type": "code" } @@ -15188,8 +15459,38 @@ ] } 06_07__inlines__links__047: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "image", + "attrs": { + "src": "moon.jpg", + "alt": "moon", + "title": null, + "uploading": false, + "canonicalSrc": null + }, + "marks": [ + { + "type": "link", + "attrs": { + "href": "/uri", + "target": "_blank", + "class": null, + "title": null, + "canonicalSrc": null + } + } + ] + } + ] + } + ] + } 06_07__inlines__links__048: |- { "type": "doc", @@ -15273,6 +15574,9 @@ "title": null, "canonicalSrc": null } + }, + { + "type": "italic" } ], "text": "baz" @@ -15847,6 +16151,18 @@ }, { "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "/url", + "target": "_blank", + "class": null, + "title": "title", + "canonicalSrc": null + } + } + ], "text": " bar" } ] @@ -15966,6 +16282,18 @@ }, { "type": "text", + "marks": [ + { + "type": "link", + "attrs": { + "href": "/url", + "target": "_blank", + "class": null, + "title": "title", + "canonicalSrc": null + } + } + ], "text": " bar" } ] @@ -16004,7 +16332,23 @@ }, { "type": "text", - "text": " bar]" + "marks": [ + { + "type": "link", + "attrs": { + "href": "/url", + "target": "_blank", + "class": null, + "title": "title", + "canonicalSrc": null + } + } + ], + "text": " bar" + }, + { + "type": "text", + "text": "]" } ] } @@ -18256,11 +18600,79 @@ ] } 06_13__inlines__hard_line_breaks__006: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "foo" + }, + { + "type": "hardBreak", + "marks": [ + { + "type": "italic" + } + ] + }, + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "\nbar" + } + ] + } + ] + } 06_13__inlines__hard_line_breaks__007: |- - Error - check implementation: - Cannot destructure property 'type' of 'this.stack.pop(...)' as it is undefined. + { + "type": "doc", + "content": [ + { + "type": "paragraph", + "content": [ + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "foo" + }, + { + "type": "hardBreak", + "marks": [ + { + "type": "italic" + } + ] + }, + { + "type": "text", + "marks": [ + { + "type": "italic" + } + ], + "text": "\nbar" + } + ] + } + ] + } 06_13__inlines__hard_line_breaks__008: |- { "type": "doc", diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb index 2b644528309..6918c087a4e 100644 --- a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb @@ -79,7 +79,7 @@ module QA Page::Project::Registry::Show.perform do |registry| expect(registry).to have_registry_repository(project.name) - registry.click_on_image(registry_repository.name) + registry.click_on_image(project.name) expect(registry).to have_tag('master') registry.click_delete diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index def7b1f5420..ddfed73e2ca 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -141,6 +141,8 @@ RSpec.describe 'Projects > Settings > Repository settings' do end context 'remote mirror settings' do + let(:ssh_url) { 'ssh://user@localhost/project.git' } + before do visit project_settings_repository_path(project) end @@ -150,11 +152,12 @@ RSpec.describe 'Projects > Settings > Repository settings' do end it 'creates a push mirror that mirrors all branches', :js do - expect(find('.js-mirror-protected-hidden', visible: false).value).to eq('0') + expect(page).to have_css('.js-mirror-protected-hidden[value="0"]', visible: false) + + fill_in 'url', with: ssh_url + expect(page).to have_css(".js-mirror-url-hidden[value=\"#{ssh_url}\"]", visible: false) - fill_in 'url', with: 'ssh://user@localhost/project.git' select 'SSH public key', from: 'Authentication method' - select_direction Sidekiq::Testing.fake! do @@ -167,13 +170,14 @@ RSpec.describe 'Projects > Settings > Repository settings' do expect(project.remote_mirrors.first.only_protected_branches).to eq(false) end - it 'creates a push mirror that only mirrors protected branches', :js, - quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/337394' do + it 'creates a push mirror that only mirrors protected branches', :js do find('#only_protected_branches').click - expect(find('.js-mirror-protected-hidden', visible: false).value).to eq('1') + expect(page).to have_css('.js-mirror-protected-hidden[value="1"]', visible: false) + + fill_in 'url', with: ssh_url + expect(page).to have_css(".js-mirror-url-hidden[value=\"#{ssh_url}\"]", visible: false) - fill_in 'url', with: 'ssh://user@localhost/project.git' select 'SSH public key', from: 'Authentication method' select_direction @@ -190,7 +194,9 @@ RSpec.describe 'Projects > Settings > Repository settings' do it 'creates a push mirror that keeps divergent refs', :js do select_direction - fill_in 'url', with: 'ssh://user@localhost/project.git' + fill_in 'url', with: ssh_url + expect(page).to have_css(".js-mirror-url-hidden[value=\"#{ssh_url}\"]", visible: false) + fill_in 'Password', with: 'password' check 'Keep divergent refs' diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js index d1690c4f6c1..cd63720a433 100644 --- a/spec/frontend/content_editor/remark_markdown_processing_spec.js +++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js @@ -931,19 +931,101 @@ Paragraph `, expectedDoc: doc(div(source('
div
'), paragraph(source('div'), 'div'))), }, + { + markdown: ` +[![moon](moon.jpg)](/uri) +`, + expectedDoc: doc( + paragraph( + source('[![moon](moon.jpg)](/uri)'), + link( + { ...source('[![moon](moon.jpg)](/uri)'), href: '/uri' }, + image({ ...source('![moon](moon.jpg)'), src: 'moon.jpg', alt: 'moon' }), + ), + ), + ), + }, + { + markdown: ` + + +*foo* + + +`, + expectedDoc: doc( + paragraph( + source('*foo*'), + strike(source('\n\n*foo*\n\n'), italic(source('*foo*'), 'foo')), + ), + ), + expectedMarkdown: '*foo*', + }, + { + markdown: ` +~[moon](moon.jpg) and [sun](sun.jpg)~ +`, + expectedDoc: doc( + paragraph( + source('~[moon](moon.jpg) and [sun](sun.jpg)~'), + strike( + source('~[moon](moon.jpg) and [sun](sun.jpg)~'), + link({ ...source('[moon](moon.jpg)'), href: 'moon.jpg' }, 'moon'), + ), + strike(source('~[moon](moon.jpg) and [sun](sun.jpg)~'), ' and '), + strike( + source('~[moon](moon.jpg) and [sun](sun.jpg)~'), + link({ ...source('[sun](sun.jpg)'), href: 'sun.jpg' }, 'sun'), + ), + ), + ), + }, + { + markdown: ` + + +**Paragraph 1** + +_Paragraph 2_ + + + `, + expectedDoc: doc( + paragraph( + source('**Paragraph 1**'), + strike( + source('\n\n**Paragraph 1**\n\n_Paragraph 2_\n\n'), + bold(source('**Paragraph 1**'), 'Paragraph 1'), + ), + ), + paragraph( + source('_Paragraph 2_'), + strike( + source('\n\n**Paragraph 1**\n\n_Paragraph 2_\n\n'), + italic(source('_Paragraph 2_'), 'Paragraph 2'), + ), + ), + ), + expectedMarkdown: `**Paragraph 1** + +_Paragraph 2_`, + }, ]; const runOnly = examples.find((example) => example.only === true); const runExamples = runOnly ? [runOnly] : examples; - it.each(runExamples)('processes %s correctly', async ({ markdown, expectedDoc }) => { - const trimmed = markdown.trim(); - const document = await deserialize(trimmed); + it.each(runExamples)( + 'processes %s correctly', + async ({ markdown, expectedDoc, expectedMarkdown }) => { + const trimmed = markdown.trim(); + const document = await deserialize(trimmed); - expect(expectedDoc).not.toBeFalsy(); - expect(document.toJSON()).toEqual(expectedDoc.toJSON()); - expect(serialize(document)).toEqual(trimmed); - }); + expect(expectedDoc).not.toBeFalsy(); + expect(document.toJSON()).toEqual(expectedDoc.toJSON()); + expect(serialize(document)).toEqual(expectedMarkdown || trimmed); + }, + ); /** * DISCLAIMER: THIS IS A SECURITY ORIENTED TEST THAT ENSURES diff --git a/spec/migrations/20220629184402_unset_escalation_policies_for_alert_incidents_spec.rb b/spec/migrations/20220629184402_unset_escalation_policies_for_alert_incidents_spec.rb new file mode 100644 index 00000000000..bd821714605 --- /dev/null +++ b/spec/migrations/20220629184402_unset_escalation_policies_for_alert_incidents_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe UnsetEscalationPoliciesForAlertIncidents do + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:issues) { table(:issues) } + let(:alerts) { table(:alert_management_alerts) } + let(:escalation_policies) { table(:incident_management_escalation_policies) } + let(:escalation_statuses) { table(:incident_management_issuable_escalation_statuses) } + let(:current_time) { Time.current.change(usec: 0) } + + let!(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') } + let!(:project_namespace) { namespaces.create!(name: 'project', path: 'project', type: 'project') } + let!(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: project_namespace.id) } + let!(:policy) { escalation_policies.create!(project_id: project.id, name: 'escalation policy') } + + # Escalation status with policy from alert; Policy & escalation start time should be nullified + let!(:issue_1) { create_issue } + let!(:escalation_status_1) { create_status(issue_1, policy, current_time) } + let!(:alert_1) { create_alert(1, issue_1) } + + # Escalation status without policy, but with alert; Should be ignored + let!(:issue_2) { create_issue } + let!(:escalation_status_2) { create_status(issue_2, nil, current_time) } + let!(:alert_2) { create_alert(2, issue_2) } + + # Escalation status without alert, but with policy; Should be ignored + let!(:issue_3) { create_issue } + let!(:escalation_status_3) { create_status(issue_3, policy, current_time) } + + # Alert without issue; Should be ignored + let!(:alert_3) { create_alert(3) } + + it 'removes the escalation policy if the incident corresponds to an alert' do + expect { migrate! } + .to change { escalation_status_1.reload.policy_id }.from(policy.id).to(nil) + .and change { escalation_status_1.escalations_started_at }.from(current_time).to(nil) + .and not_change { policy_attrs(escalation_status_2) } + .and not_change { policy_attrs(escalation_status_3) } + end + + private + + def create_issue + issues.create!(project_id: project.id) + end + + def create_status(issue, policy = nil, escalations_started_at = nil) + escalation_statuses.create!( + issue_id: issue.id, + policy_id: policy&.id, + escalations_started_at: escalations_started_at + ) + end + + def create_alert(iid, issue = nil) + alerts.create!( + project_id: project.id, + started_at: current_time, + title: "alert #{iid}", + iid: iid.to_s, + issue_id: issue&.id + ) + end + + def policy_attrs(escalation_status) + escalation_status.reload.slice(:policy_id, :escalations_started_at) + end +end diff --git a/spec/models/packages/cleanup/policy_spec.rb b/spec/models/packages/cleanup/policy_spec.rb index c08ae4aa7e7..9f4568932d2 100644 --- a/spec/models/packages/cleanup/policy_spec.rb +++ b/spec/models/packages/cleanup/policy_spec.rb @@ -25,4 +25,16 @@ RSpec.describe Packages::Cleanup::Policy, type: :model do it { is_expected.to contain_exactly(active_policy) } end + + describe '#keep_n_duplicated_package_files_disabled?' do + subject { policy.keep_n_duplicated_package_files_disabled? } + + %w[all 1].each do |value| + context "with value set to #{value}" do + let(:policy) { build(:packages_cleanup_policy, keep_n_duplicated_package_files: value) } + + it { is_expected.to eq(value == 'all') } + end + end + end end diff --git a/spec/services/alert_management/alerts/update_service_spec.rb b/spec/services/alert_management/alerts/update_service_spec.rb index 9bdc9970807..8375c8cdf7d 100644 --- a/spec/services/alert_management/alerts/update_service_spec.rb +++ b/spec/services/alert_management/alerts/update_service_spec.rb @@ -249,57 +249,6 @@ RSpec.describe AlertManagement::Alerts::UpdateService do it_behaves_like 'adds a system note' end - - context 'with an associated issue' do - let_it_be(:issue, reload: true) { create(:issue, project: project) } - - before do - alert.update!(issue: issue) - end - - shared_examples 'does not sync with the incident status' do - specify do - expect(::Issues::UpdateService).not_to receive(:new) - expect { response }.to change { alert.acknowledged? }.to(true) - end - end - - it_behaves_like 'does not sync with the incident status' - - context 'when the issue is an incident' do - before do - issue.update!(issue_type: Issue.issue_types[:incident]) - end - - it_behaves_like 'does not sync with the incident status' - - context 'when the incident has an escalation status' do - let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, issue: issue) } - - it 'updates the incident escalation status with the new alert status' do - expect(::Issues::UpdateService).to receive(:new).once.and_call_original - expect(described_class).to receive(:new).once.and_call_original - - expect { response }.to change { escalation_status.reload.acknowledged? }.to(true) - .and change { alert.reload.acknowledged? }.to(true) - end - - context 'when the statuses match' do - before do - escalation_status.update!(status_event: :acknowledge) - end - - it_behaves_like 'does not sync with the incident status' - end - end - end - end - - context 'when a status change reason is included' do - let(:params) { { status: new_status, status_change_reason: ' by changing the incident status' } } - - it_behaves_like 'adds a system note', /changed the status to \*\*Acknowledged\*\* by changing the incident status/ - end end end end diff --git a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb index 88c5f6125b9..4b0c8d9113c 100644 --- a/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb +++ b/spec/services/incident_management/issuable_escalation_statuses/after_update_service_spec.rb @@ -7,14 +7,11 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateServic let_it_be(:escalation_status, reload: true) { create(:incident_management_issuable_escalation_status, :triggered) } let_it_be(:issue, reload: true) { escalation_status.issue } let_it_be(:project) { issue.project } - let_it_be(:alert) { create(:alert_management_alert, issue: issue, project: project) } - let(:status_event) { :acknowledge } - let(:update_params) { { incident_management_issuable_escalation_status_attributes: { status_event: status_event } } } let(:service) { IncidentManagement::IssuableEscalationStatuses::AfterUpdateService.new(issue, current_user) } subject(:result) do - issue.update!(update_params) + issue.update!(incident_management_issuable_escalation_status_attributes: update_params) service.execute end @@ -22,22 +19,15 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateServic issue.project.add_developer(current_user) end - shared_examples 'does not attempt to update the alert' do - specify do - expect(::AlertManagement::Alerts::UpdateService).not_to receive(:new) + context 'with status attributes' do + let(:status_event) { :acknowledge } + let(:update_params) { { status_event: status_event } } - expect(result).to be_success - end - end - - shared_examples 'adds a status change system note' do - specify do + it 'adds a status change system note' do expect { result }.to change { issue.reload.notes.count }.by(1) end - end - shared_examples 'adds a status change timeline event' do - specify do + it 'adds a status change timeline event' do expect(IncidentManagement::TimelineEvents::CreateService) .to receive(:change_incident_status) .with(issue, current_user, escalation_status) @@ -47,35 +37,13 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::AfterUpdateServic end end - context 'with status attributes' do - it_behaves_like 'adds a status change system note' - it_behaves_like 'adds a status change timeline event' + context 'with non-status attributes' do + let(:update_params) { { updated_at: Time.current } } - it 'updates the alert with the new alert status' do - expect(::AlertManagement::Alerts::UpdateService).to receive(:new).once.and_call_original - expect(described_class).to receive(:new).once.and_call_original - - expect { result }.to change { escalation_status.reload.acknowledged? }.to(true) - .and change { alert.reload.acknowledged? }.to(true) - end - - context 'when incident is not associated with an alert' do - before do - alert.destroy! - end - - it_behaves_like 'does not attempt to update the alert' - it_behaves_like 'adds a status change system note' - it_behaves_like 'adds a status change timeline event' - end - - context 'when new status matches the current status' do - let(:status_event) { :trigger } - - it_behaves_like 'does not attempt to update the alert' - - specify { expect { result }.not_to change { issue.reload.notes.count } } - specify { expect { result }.not_to change { issue.reload.incident_management_timeline_events.count } } + it 'does not add a status change system note or timeline event' do + expect { result } + .to not_change { issue.reload.notes.count } + .and not_change { issue.reload.incident_management_timeline_events.count } end end end diff --git a/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb index c20a0688ac2..b5c5238d483 100644 --- a/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb +++ b/spec/services/incident_management/issuable_escalation_statuses/build_service_spec.rb @@ -11,10 +11,4 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::BuildService do subject(:execute) { service.execute } it_behaves_like 'initializes new escalation status with expected attributes' - - context 'with associated alert' do - let_it_be(:alert) { create(:alert_management_alert, :acknowledged, project: project, issue: incident) } - - it_behaves_like 'initializes new escalation status with expected attributes', { status_event: :acknowledge } - end end diff --git a/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb index 2c7d330766c..b6ae03a19fe 100644 --- a/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb +++ b/spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb @@ -27,19 +27,4 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::CreateService do expect { execute }.not_to change { incident.reload.escalation_status } end end - - context 'with associated alert' do - before do - create(:alert_management_alert, :acknowledged, project: project, issue: incident) - end - - it 'creates an escalation status matching the alert attributes' do - expect { execute }.to change { incident.reload.escalation_status }.from(nil) - expect(incident.escalation_status).to have_attributes( - status_name: :acknowledged, - policy_id: nil, - escalations_started_at: nil - ) - end - end end diff --git a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb index 6c99631fcb0..761cc5c92ea 100644 --- a/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb +++ b/spec/services/incident_management/issuable_escalation_statuses/prepare_update_service_spec.rb @@ -102,10 +102,4 @@ RSpec.describe IncidentManagement::IssuableEscalationStatuses::PrepareUpdateServ it_behaves_like 'successful response', { status_event: :acknowledge } end end - - context 'with status_change_reason param' do - let(:params) { { status_change_reason: ' by changing the incident status' } } - - it_behaves_like 'successful response', { status_change_reason: ' by changing the incident status' } - end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index d11fe772023..e2e8828ae89 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1146,11 +1146,11 @@ RSpec.describe Issues::UpdateService, :mailer do let(:opts) { { escalation_status: { status: 'acknowledged' } } } let(:escalation_update_class) { ::IncidentManagement::IssuableEscalationStatuses::AfterUpdateService } - shared_examples 'updates the escalation status record' do |expected_status, expected_reason = nil| + shared_examples 'updates the escalation status record' do |expected_status| let(:service_double) { instance_double(escalation_update_class) } it 'has correct value' do - expect(escalation_update_class).to receive(:new).with(issue, user, status_change_reason: expected_reason).and_return(service_double) + expect(escalation_update_class).to receive(:new).with(issue, user).and_return(service_double) expect(service_double).to receive(:execute) update_issue(opts) @@ -1193,23 +1193,6 @@ RSpec.describe Issues::UpdateService, :mailer do it_behaves_like 'updates the escalation status record', :acknowledged - context 'with associated alert' do - let!(:alert) { create(:alert_management_alert, issue: issue, project: project) } - - it 'syncs the update back to the alert' do - update_issue(opts) - - expect(issue.escalation_status.status_name).to eq(:acknowledged) - expect(alert.reload.status_name).to eq(:acknowledged) - end - end - - context 'with a status change reason provided' do - let(:opts) { { escalation_status: { status: 'acknowledged', status_change_reason: ' by changing the alert status' } } } - - it_behaves_like 'updates the escalation status record', :acknowledged, ' by changing the alert status' - end - context 'with unsupported status value' do let(:opts) { { escalation_status: { status: 'unsupported-status' } } } diff --git a/spec/services/packages/cleanup/execute_policy_service_spec.rb b/spec/services/packages/cleanup/execute_policy_service_spec.rb new file mode 100644 index 00000000000..93335c4a821 --- /dev/null +++ b/spec/services/packages/cleanup/execute_policy_service_spec.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true +require 'spec_helper' + +RSpec.describe Packages::Cleanup::ExecutePolicyService do + let_it_be(:project) { create(:project) } + let_it_be_with_reload(:policy) { create(:packages_cleanup_policy, project: project) } + + let(:service) { described_class.new(policy) } + + describe '#execute' do + subject(:execute) { service.execute } + + context 'with the keep_n_duplicated_files parameter' do + let_it_be(:package1) { create(:package, project: project) } + let_it_be(:package2) { create(:package, project: project) } + let_it_be(:package3) { create(:package, project: project) } + let_it_be(:package4) { create(:package, :pending_destruction, project: project) } + + let_it_be(:package_file1_1) { create(:package_file, package: package1, file_name: 'file_name1') } + let_it_be(:package_file1_2) { create(:package_file, package: package1, file_name: 'file_name1') } + let_it_be(:package_file1_3) { create(:package_file, package: package1, file_name: 'file_name1') } + + let_it_be(:package_file1_4) { create(:package_file, package: package1, file_name: 'file_name2') } + let_it_be(:package_file1_5) { create(:package_file, package: package1, file_name: 'file_name2') } + let_it_be(:package_file1_6) { create(:package_file, package: package1, file_name: 'file_name2') } + let_it_be(:package_file1_7) do + create(:package_file, :pending_destruction, package: package1, file_name: 'file_name2') + end + + let_it_be(:package_file2_1) { create(:package_file, package: package2, file_name: 'file_name1') } + let_it_be(:package_file2_2) { create(:package_file, package: package2, file_name: 'file_name1') } + let_it_be(:package_file2_3) { create(:package_file, package: package2, file_name: 'file_name1') } + let_it_be(:package_file2_4) { create(:package_file, package: package2, file_name: 'file_name1') } + + let_it_be(:package_file3_1) { create(:package_file, package: package3, file_name: 'file_name_test') } + + let_it_be(:package_file4_1) { create(:package_file, package: package4, file_name: 'file_name1') } + let_it_be(:package_file4_2) { create(:package_file, package: package4, file_name: 'file_name1') } + + let(:package_files_1) { package1.package_files.installable } + let(:package_files_2) { package2.package_files.installable } + let(:package_files_3) { package3.package_files.installable } + + context 'set to less than the total number of duplicated files' do + before do + # for each package file duplicate, we keep only the most recent one + policy.update!(keep_n_duplicated_package_files: '1') + end + + shared_examples 'keeping the most recent package files' do + let(:response_payload) do + { + counts: { + marked_package_files_total_count: 7, + unique_package_id_and_file_name_total_count: 3 + }, + timeout: false + } + end + + it 'only keeps the most recent package files' do + expect { execute }.to change { ::Packages::PackageFile.installable.count }.by(-7) + + expect(package_files_1).to contain_exactly(package_file1_3, package_file1_6) + expect(package_files_2).to contain_exactly(package_file2_4) + expect(package_files_3).to contain_exactly(package_file3_1) + + expect(execute).to be_success + expect(execute.message).to eq("Packages cleanup policy executed for project #{project.id}") + expect(execute.payload).to eq(response_payload) + end + end + + it_behaves_like 'keeping the most recent package files' + + context 'when the service needs to loop' do + before do + stub_const("#{described_class.name}::DUPLICATED_FILES_BATCH_SIZE", 2) + end + + it_behaves_like 'keeping the most recent package files' do + before do + expect(::Packages::MarkPackageFilesForDestructionService) + .to receive(:new).exactly(3).times.and_call_original + end + end + + context 'when a timeout is hit' do + let(:response_payload) do + { + counts: { + marked_package_files_total_count: 4, + unique_package_id_and_file_name_total_count: 3 + }, + timeout: true + } + end + + let(:service_timeout_response) do + ServiceResponse.error( + message: 'Timeout while marking package files as pending destruction', + payload: { marked_package_files_count: 0 } + ) + end + + before do + mock_service_timeout(on_iteration: 3) + end + + it 'keeps part of the most recent package files' do + expect { execute } + .to change { ::Packages::PackageFile.installable.count }.by(-4) + .and not_change { package_files_2.count } # untouched because of the timeout + .and not_change { package_files_3.count } # untouched because of the timeout + + expect(package_files_1).to contain_exactly(package_file1_3, package_file1_6) + expect(execute).to be_success + expect(execute.message).to eq("Packages cleanup policy executed for project #{project.id}") + expect(execute.payload).to eq(response_payload) + end + + def mock_service_timeout(on_iteration:) + execute_call_count = 1 + expect_next_instances_of(::Packages::MarkPackageFilesForDestructionService, 3) do |service| + expect(service).to receive(:execute).and_wrap_original do |m, *args| + # timeout if we are on the right iteration + if execute_call_count == on_iteration + service_timeout_response + else + execute_call_count += 1 + m.call(*args) + end + end + end + end + end + end + end + + context 'set to more than the total number of duplicated files' do + before do + # using the biggest value for keep_n_duplicated_package_files + policy.update!(keep_n_duplicated_package_files: '50') + end + + it 'keeps all package files' do + expect { execute }.not_to change { ::Packages::PackageFile.installable.count } + end + end + + context 'set to all' do + before do + policy.update!(keep_n_duplicated_package_files: 'all') + end + + it 'skips the policy' do + expect(::Packages::MarkPackageFilesForDestructionService).not_to receive(:new) + expect { execute }.not_to change { ::Packages::PackageFile.installable.count } + end + end + end + end +end diff --git a/spec/services/packages/mark_package_files_for_destruction_service_spec.rb b/spec/services/packages/mark_package_files_for_destruction_service_spec.rb index a836de1f7f6..66534338003 100644 --- a/spec/services/packages/mark_package_files_for_destruction_service_spec.rb +++ b/spec/services/packages/mark_package_files_for_destruction_service_spec.rb @@ -6,9 +6,11 @@ RSpec.describe Packages::MarkPackageFilesForDestructionService, :aggregate_failu let(:service) { described_class.new(package_files) } describe '#execute', :aggregate_failures do - subject { service.execute } + let(:batch_deadline) { nil } - shared_examples 'executing successfully' do + subject { service.execute(batch_deadline: batch_deadline) } + + shared_examples 'executing successfully' do |marked_package_files_count: 0| it 'marks package files for destruction' do expect { subject } .to change { ::Packages::PackageFile.pending_destruction.count }.by(package_files.size) @@ -17,6 +19,7 @@ RSpec.describe Packages::MarkPackageFilesForDestructionService, :aggregate_failu it 'executes successfully' do expect(subject).to be_success expect(subject.message).to eq('Package files are now pending destruction') + expect(subject.payload).to eq(marked_package_files_count: marked_package_files_count) end end @@ -30,13 +33,49 @@ RSpec.describe Packages::MarkPackageFilesForDestructionService, :aggregate_failu let_it_be(:package_file) { create(:package_file) } let_it_be(:package_files) { ::Packages::PackageFile.id_in(package_file.id) } - it_behaves_like 'executing successfully' + it_behaves_like 'executing successfully', marked_package_files_count: 1 end context 'with many package files' do let_it_be(:package_files) { ::Packages::PackageFile.id_in(create_list(:package_file, 3).map(&:id)) } - it_behaves_like 'executing successfully' + it_behaves_like 'executing successfully', marked_package_files_count: 3 + + context 'with a batch deadline' do + let_it_be(:batch_deadline) { 250.seconds.from_now } + + context 'when the deadline is not hit' do + before do + expect(Time.zone).to receive(:now).and_return(batch_deadline - 10.seconds) + end + + it_behaves_like 'executing successfully', marked_package_files_count: 3 + end + + context 'when the deadline is hit' do + it 'does not execute the batch loop' do + expect(Time.zone).to receive(:now).and_return(batch_deadline + 10.seconds) + expect { subject }.to not_change { ::Packages::PackageFile.pending_destruction.count } + expect(subject).to be_error + expect(subject.message).to eq('Timeout while marking package files as pending destruction') + expect(subject.payload).to eq(marked_package_files_count: 0) + end + end + end + + context 'when a batch size is defined' do + let_it_be(:batch_deadline) { 250.seconds.from_now } + + let(:batch_size) { 2 } + + subject { service.execute(batch_deadline: batch_deadline, batch_size: batch_size) } + + before do + expect(Time.zone).to receive(:now).twice.and_call_original + end + + it_behaves_like 'executing successfully', marked_package_files_count: 3 + end end context 'with an error during the update' do