summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.vale.ini39
-rw-r--r--app/assets/javascripts/api.js6
-rw-r--r--app/assets/javascripts/environments/components/enable_review_app_button.vue8
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue3
-rw-r--r--app/assets/javascripts/lib/utils/icon_utils.js44
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss146
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss224
-rw-r--r--app/services/system_note_service.rb4
-rw-r--r--app/services/system_notes/issuables_service.rb6
-rw-r--r--changelogs/unreleased/stop_environments.yml5
-rw-r--r--doc/.vale/gitlab/Contractions.yml (renamed from doc/.linting/vale/styles/gitlab/Contractions.yml)0
-rw-r--r--doc/.vale/gitlab/FirstPerson.yml (renamed from doc/.linting/vale/styles/gitlab/FirstPerson.yml)0
-rw-r--r--doc/.vale/gitlab/InternalLinkExtension.yml (renamed from doc/.linting/vale/styles/gitlab/InternalLinkExtension.yml)0
-rw-r--r--doc/.vale/gitlab/LatinTerms.yml (renamed from doc/.linting/vale/styles/gitlab/LatinTerms.yml)0
-rw-r--r--doc/.vale/gitlab/OxfordComma.yml (renamed from doc/.linting/vale/styles/gitlab/OxfordComma.yml)0
-rw-r--r--doc/.vale/gitlab/RelativeLinks.yml (renamed from doc/.linting/vale/styles/gitlab/RelativeLinks.yml)0
-rw-r--r--doc/.vale/gitlab/SentenceSpacing.yml (renamed from doc/.linting/vale/styles/gitlab/SentenceSpacing.yml)0
-rw-r--r--doc/.vale/gitlab/Substitutions.yml (renamed from doc/.linting/vale/styles/gitlab/Substitutions.yml)0
-rw-r--r--doc/ci/review_apps/img/enable_review_app_v12_8.pngbin17151 -> 14013 bytes
-rw-r--r--doc/development/contributing/issue_workflow.md2
-rw-r--r--doc/development/event_tracking/backend.md5
-rw-r--r--doc/development/event_tracking/frontend.md5
-rw-r--r--doc/development/event_tracking/index.md5
-rw-r--r--doc/policy/maintenance.md10
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml1
-rw-r--r--lib/gitlab/import_export/group/tree_restorer.rb25
-rw-r--r--lib/gitlab/import_export/json/legacy_reader.rb104
-rw-r--r--lib/gitlab/import_export/project/tree_loader.rb74
-rw-r--r--lib/gitlab/import_export/project/tree_restorer.rb22
-rw-r--r--lib/gitlab/import_export/reader.rb8
-rw-r--r--lib/gitlab/import_export/relation_tree_restorer.rb34
-rw-r--r--lib/gitlab/import_export/relation_tree_saver.rb2
-rw-r--r--spec/fixtures/lib/gitlab/import_export/invalid_json/project.json3
-rw-r--r--spec/frontend/__mocks__/mousetrap/index.js6
-rw-r--r--spec/frontend/diffs/components/app_spec.js (renamed from spec/javascripts/diffs/components/app_spec.js)91
-rw-r--r--spec/frontend/diffs/create_diffs_store.js15
-rw-r--r--spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap1
-rw-r--r--spec/frontend/lib/utils/icon_utils_spec.js72
-rw-r--r--spec/frontend/mocks/ce/diffs/workers/tree_worker.js8
-rw-r--r--spec/javascripts/diffs/create_diffs_store.js16
-rw-r--r--spec/lib/gitlab/import_export/json/legacy_reader_spec.rb149
-rw-r--r--spec/lib/gitlab/import_export/project/tree_loader_spec.rb49
-rw-r--r--spec/lib/gitlab/import_export/project/tree_restorer_spec.rb3
-rw-r--r--spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb27
-rw-r--r--spec/services/system_note_service_spec.rb10
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb12
47 files changed, 719 insertions, 527 deletions
diff --git a/.vale.ini b/.vale.ini
index 89a669ec7ff..13b198b9148 100644
--- a/.vale.ini
+++ b/.vale.ini
@@ -1,40 +1,9 @@
-# Vale configuration file, taken from https://errata-ai.github.io/vale/config/
+# Vale configuration file.
+#
+# For more information, see https://errata-ai.gitbook.io/vale/getting-started/configuration.
-# The relative path to the folder containing linting rules (styles)
-# -----------------------------------------------------------------
-StylesPath = doc/.linting/vale/styles
-
-# Minimum alert level
-# -------------------
-# The minimum alert level to display (suggestion, warning, or error).
-# If integrated into CI, builds fail by default on error-level alerts,
-# unless you execute Vale with the --no-exit flag
+StylesPath = doc/.vale
MinAlertLevel = suggestion
-# Should Vale parse any file formats other than .md files as Markdown?
-# --------------------------------------------------------------------
-[formats]
-mdx = md
-
-# What file types should Vale test?
-# ----------------------------------
[*.md]
-
-# Styles to load
-# --------------
-# What styles, located in the StylesPath folder, should Vale load?
-# Vale also currently includes write-good, proselint, joblint, and vale
BasedOnStyles = gitlab
-
-# Enabling or disabling specific rules in a style
-# -----------------------------------------------
-# To disable a rule in an enabled style, use the following format:
-# {style}.{filename} = NO
-# To enable a single rule in a disabled style, use the following format:
-# vale.Editorializing = YES
-
-# Altering the severity of a rule in a style
-# ------------------------------------------
-# To change the reporting level (suggestion, warning, error) of a rule,
-# use the following format: {style}.{filename} = {level}
-# vale.Hedging = error
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index dc6ea148047..022d79ecf49 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,5 +1,3 @@
-import $ from 'jquery';
-import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { joinPaths } from './lib/utils/url_utility';
import flash from '~/flash';
@@ -70,7 +68,7 @@ const Api = {
},
// Return groups list. Filtered by query
- groups(query, options, callback = $.noop) {
+ groups(query, options, callback = () => {}) {
const url = Api.buildUrl(Api.groupsPath);
return axios
.get(url, {
@@ -108,7 +106,7 @@ const Api = {
},
// Return projects list. Filtered by query
- projects(query, options, callback = _.noop) {
+ projects(query, options, callback = () => {}) {
const url = Api.buildUrl(Api.projectsPath);
const defaults = {
search: query,
diff --git a/app/assets/javascripts/environments/components/enable_review_app_button.vue b/app/assets/javascripts/environments/components/enable_review_app_button.vue
index 2f9e9cb628f..8fbbc5189bf 100644
--- a/app/assets/javascripts/environments/components/enable_review_app_button.vue
+++ b/app/assets/javascripts/environments/components/enable_review_app_button.vue
@@ -26,15 +26,17 @@ export default {
modalInfo: {
closeText: s__('EnableReviewApp|Close'),
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
- copyString: `deploy_review
+ copyString: `deploy_review:
stage: deploy
script:
- echo "Deploy a review app"
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
- only: branches
- except: master`,
+ only:
+ - branches
+ except:
+ - master`,
id: 'enable-review-app-info',
title: s__('ReviewApp|Enable Review App'),
},
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 2e273d45506..a15e22d4742 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -94,7 +94,7 @@ export default {
data-boundary="viewport"
@click="openDiscardModal"
>
- <icon :size="16" name="remove-all" class="ml-auto mr-auto" />
+ <icon :size="16" name="remove-all" class="ml-auto mr-auto position-top-0" />
</button>
</div>
</div>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index b61d0a47795..3a63fc32639 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -59,7 +59,7 @@ export default {
<gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" />
<template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
- <ci-icon :status="latestPipeline.details.status" :size="24" />
+ <ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" />
<span class="prepend-left-8">
<strong> {{ __('Pipeline') }} </strong>
<a
@@ -76,6 +76,7 @@ export default {
:help-page-path="links.ciHelpPagePath"
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
:can-set-ci="true"
+ class="mb-auto mt-auto"
/>
<div v-else-if="latestPipeline.yamlError" class="bs-callout bs-callout-danger">
<p class="append-bottom-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p>
diff --git a/app/assets/javascripts/lib/utils/icon_utils.js b/app/assets/javascripts/lib/utils/icon_utils.js
index 7b8dd9bbef7..97ee773358d 100644
--- a/app/assets/javascripts/lib/utils/icon_utils.js
+++ b/app/assets/javascripts/lib/utils/icon_utils.js
@@ -1,18 +1,40 @@
-/* eslint-disable import/prefer-default-export */
-
+import { memoize } from 'lodash';
import axios from '~/lib/utils/axios_utils';
/**
- * Retrieve SVG icon path content from gitlab/svg sprite icons
- * @param {String} name
+ * Resolves to a DOM that contains GitLab icons
+ * in svg format. Memoized to avoid duplicate requests
*/
-export const getSvgIconPathContent = name =>
+const getSvgDom = memoize(() =>
axios
.get(gon.sprite_icons)
- .then(({ data: svgs }) =>
- new DOMParser()
- .parseFromString(svgs, 'text/xml')
- .querySelector(`#${name} path`)
- .getAttribute('d'),
- )
+ .then(({ data: svgs }) => new DOMParser().parseFromString(svgs, 'text/xml'))
+ .catch(() => {
+ getSvgDom.cache.clear();
+ }),
+);
+
+/**
+ * Clears the memoized SVG content.
+ *
+ * You probably don't need to invoke this function unless
+ * sprite_icons are updated.
+ */
+export const clearSvgIconPathContentCache = () => {
+ getSvgDom.cache.clear();
+};
+
+/**
+ * Retrieve SVG icon path content from gitlab/svg sprite icons.
+ *
+ * Content loaded is cached.
+ *
+ * @param {String} name - Icon name
+ * @returns A promise that resolves to the svg path
+ */
+export const getSvgIconPathContent = name =>
+ getSvgDom()
+ .then(doc => {
+ return doc.querySelector(`#${name} path`).getAttribute('d');
+ })
.catch(() => null);
diff --git a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
new file mode 100644
index 00000000000..c47901dc177
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
@@ -0,0 +1,146 @@
+
+// stylelint-disable selector-class-pattern
+// stylelint-disable selector-max-compound-selectors
+// stylelint-disable stylelint-gitlab/duplicate-selectors
+// stylelint-disable stylelint-gitlab/utility-classes
+
+.blob-editor-container {
+ flex: 1;
+ height: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .vertical-center {
+ min-height: auto;
+ }
+
+ .monaco-editor .lines-content .cigr {
+ display: none;
+ }
+
+ .monaco-editor .selected-text {
+ z-index: 1;
+ }
+
+ .monaco-editor .view-lines {
+ z-index: 2;
+ }
+
+ .is-readonly,
+ .editor.original {
+ .view-lines {
+ cursor: default;
+ }
+
+ .cursors-layer {
+ display: none;
+ }
+ }
+
+ .is-deleted {
+ .editor.modified {
+ .margin-view-overlays,
+ .lines-content,
+ .decorationsOverviewRuler {
+ // !important to override monaco inline styles
+ display: none !important;
+ }
+ }
+
+ .diffOverviewRuler.modified {
+ // !important to override monaco inline styles
+ display: none !important;
+ }
+ }
+
+ .is-added {
+ .editor.original {
+ .margin-view-overlays,
+ .lines-content,
+ .decorationsOverviewRuler {
+ // !important to override monaco inline styles
+ display: none !important;
+ }
+ }
+
+ .diffOverviewRuler.original {
+ // !important to override monaco inline styles
+ display: none !important;
+ }
+ }
+
+ .monaco-diff-editor.vs {
+ .editor.modified {
+ box-shadow: none;
+ }
+
+ .diagonal-fill {
+ display: none !important;
+ }
+
+ .diffOverview {
+ background-color: $white-light;
+ border-left: 1px solid $white-dark;
+ cursor: ns-resize;
+ }
+
+ .diffViewport {
+ display: none;
+ }
+
+ .char-insert {
+ background-color: $line-added-dark;
+ }
+
+ .char-delete {
+ background-color: $line-removed-dark;
+ }
+
+ .line-numbers {
+ color: $black-transparent;
+ }
+
+ .view-overlays {
+ .line-insert {
+ background-color: $line-added;
+ }
+
+ .line-delete {
+ background-color: $line-removed;
+ }
+ }
+
+ .margin {
+ background-color: $white-light;
+ border-right: 1px solid $gray-100;
+
+ .line-insert {
+ border-right: 1px solid $line-added-dark;
+ }
+
+ .line-delete {
+ border-right: 1px solid $line-removed-dark;
+ }
+ }
+
+ .margin-view-overlays .insert-sign,
+ .margin-view-overlays .delete-sign {
+ opacity: 0.4;
+ }
+ }
+}
+
+.multi-file-editor-holder {
+ height: 100%;
+ min-height: 0; // firefox fix
+
+ &.is-readonly .vs,
+ .vs .editor.original {
+ .monaco-editor,
+ .monaco-editor-background,
+ .monaco-editor .inputarea.ime-input {
+ background-color: $gray-50;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index a748c669ee8..c37f75d1533 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1,6 +1,7 @@
@import 'framework/variables';
@import 'framework/mixins';
@import './ide_mixins';
+@import './ide_monaco_overrides';
$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px;
@@ -16,11 +17,6 @@ $ide-commit-header-height: 48px;
display: inline-block;
}
-.fade-enter,
-.fade-leave-to {
- opacity: 0;
-}
-
.commit-message {
@include str-truncated(250px);
}
@@ -49,10 +45,6 @@ $ide-commit-header-height: 48px;
flex-direction: column;
flex: 1;
min-height: 0; // firefox fix
-
- a {
- color: $gl-text-color;
- }
}
.multi-file-loading-container {
@@ -160,157 +152,6 @@ $ide-commit-header-height: 48px;
height: 0;
}
-// stylelint-disable selector-class-pattern
-// stylelint-disable selector-max-compound-selectors
-// stylelint-disable stylelint-gitlab/duplicate-selectors
-// stylelint-disable stylelint-gitlab/utility-classes
-
-.blob-editor-container {
- flex: 1;
- height: 0;
- display: flex;
- flex-direction: column;
- justify-content: center;
-
- .vertical-center {
- min-height: auto;
- }
-
- .monaco-editor .lines-content .cigr {
- display: none;
- }
-
- .monaco-editor .selected-text {
- z-index: 1;
- }
-
- .monaco-editor .view-lines {
- z-index: 2;
- }
-
- .is-readonly,
- .editor.original {
- .view-lines {
- cursor: default;
- }
-
- .cursors-layer {
- display: none;
- }
- }
-
- .is-deleted {
- .editor.modified {
- .margin-view-overlays,
- .lines-content,
- .decorationsOverviewRuler {
- // !important to override monaco inline styles
- display: none !important;
- }
- }
-
- .diffOverviewRuler.modified {
- // !important to override monaco inline styles
- display: none !important;
- }
- }
-
- .is-added {
- .editor.original {
- .margin-view-overlays,
- .lines-content,
- .decorationsOverviewRuler {
- // !important to override monaco inline styles
- display: none !important;
- }
- }
-
- .diffOverviewRuler.original {
- // !important to override monaco inline styles
- display: none !important;
- }
- }
-
- .monaco-diff-editor.vs {
- .editor.modified {
- box-shadow: none;
- }
-
- .diagonal-fill {
- display: none !important;
- }
-
- .diffOverview {
- background-color: $white-light;
- border-left: 1px solid $white-dark;
- cursor: ns-resize;
- }
-
- .diffViewport {
- display: none;
- }
-
- .char-insert {
- background-color: $line-added-dark;
- }
-
- .char-delete {
- background-color: $line-removed-dark;
- }
-
- .line-numbers {
- color: $black-transparent;
- }
-
- .view-overlays {
- .line-insert {
- background-color: $line-added;
- }
-
- .line-delete {
- background-color: $line-removed;
- }
- }
-
- .margin {
- background-color: $white-light;
- border-right: 1px solid $gray-100;
-
- .line-insert {
- border-right: 1px solid $line-added-dark;
- }
-
- .line-delete {
- border-right: 1px solid $line-removed-dark;
- }
- }
-
- .margin-view-overlays .insert-sign,
- .margin-view-overlays .delete-sign {
- opacity: 0.4;
- }
- }
-}
-
-.multi-file-editor-holder {
- height: 100%;
- min-height: 0; // firefox fix
-
- &.is-readonly .vs,
- .vs .editor.original {
- .monaco-editor,
- .monaco-editor-background,
- .monaco-editor .inputarea.ime-input {
- background-color: $gray-50;
- }
- }
-}
-
-// stylelint-enable selector-class-pattern
-// stylelint-enable selector-max-compound-selectors
-// stylelint-enable stylelint-gitlab/duplicate-selectors
-// stylelint-enable stylelint-gitlab/utility-classes
-
.preview-container {
flex-grow: 1;
position: relative;
@@ -671,10 +512,6 @@ $ide-commit-header-height: 48px;
width: $ide-commit-row-height;
height: $ide-commit-row-height;
color: inherit;
-
- > svg {
- top: 0;
- }
}
.ide-commit-file-count {
@@ -864,39 +701,39 @@ $ide-commit-header-height: 48px;
margin-left: auto;
}
- .ide-nav-dropdown {
- width: 100%;
- margin-bottom: 12px;
+ button {
+ color: $gl-text-color;
+ }
+}
- .dropdown-menu {
- width: 385px;
- max-height: initial;
- }
+.ide-nav-dropdown {
+ width: 100%;
+ margin-bottom: 12px;
- .dropdown-menu-toggle {
- svg {
- vertical-align: middle;
- color: $gray-700;
+ .dropdown-menu {
+ width: 385px;
+ max-height: initial;
+ }
- &:hover {
- color: $gray-700;
- }
- }
+ .dropdown-menu-toggle {
+ svg {
+ vertical-align: middle;
+ color: $gray-700;
&:hover {
- background-color: $white-normal;
+ color: $gray-700;
}
}
- &.show {
- .dropdown-menu-toggle {
- background-color: $white-dark;
- }
+ &:hover {
+ background-color: $white-normal;
}
}
- button {
- color: $gl-text-color;
+ &.show {
+ .dropdown-menu-toggle {
+ background-color: $white-dark;
+ }
}
}
@@ -945,6 +782,8 @@ $ide-commit-header-height: 48px;
transform: translateY(0);
}
+.fade-enter,
+.fade-leave-to,
.commit-form-slide-up-enter,
.commit-form-slide-up-leave-to {
opacity: 0;
@@ -1063,9 +902,6 @@ $ide-commit-header-height: 48px;
@include ide-trace-view();
.empty-state {
- margin-top: auto;
- margin-bottom: auto;
-
p {
margin: $grid-size 0;
text-align: center;
@@ -1092,10 +928,6 @@ $ide-commit-header-height: 48px;
min-height: 55px;
padding-left: $gl-padding;
padding-right: $gl-padding;
-
- .ci-status-icon {
- display: flex;
- }
}
.ide-job-item {
@@ -1135,7 +967,7 @@ $ide-commit-header-height: 48px;
}
.ide-nav-form {
- .nav-links li {
+ li {
width: 50%;
padding-left: 0;
padding-right: 0;
@@ -1222,10 +1054,6 @@ $ide-commit-header-height: 48px;
background-color: $blue-500;
outline: 0;
}
-
- svg {
- fill: currentColor;
- }
}
.ide-new-btn {
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 8a0f44b4e93..1b9f5971f73 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -241,6 +241,10 @@ module SystemNoteService
def zoom_link_removed(issue, project, author)
::SystemNotes::ZoomService.new(noteable: issue, project: project, author: author).zoom_link_removed
end
+
+ def auto_resolve_prometheus_alert(noteable, project, author)
+ ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).auto_resolve_prometheus_alert
+ end
end
SystemNoteService.prepend_if_ee('EE::SystemNoteService')
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index b555760f88e..275c64bea89 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -288,6 +288,12 @@ module SystemNotes
create_note(NoteSummary.new(noteable, project, author, body, action: 'closed'))
end
+ def auto_resolve_prometheus_alert
+ body = 'automatically closed this issue because the alert resolved.'
+
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'closed'))
+ end
+
private
def cross_reference_note_content(gfm_reference)
diff --git a/changelogs/unreleased/stop_environments.yml b/changelogs/unreleased/stop_environments.yml
new file mode 100644
index 00000000000..ea92be202af
--- /dev/null
+++ b/changelogs/unreleased/stop_environments.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fixes stop_review job upon expired artifacts from previous stages'
+merge_request: 27258
+author: Jack Lei
+type: fixed
diff --git a/doc/.linting/vale/styles/gitlab/Contractions.yml b/doc/.vale/gitlab/Contractions.yml
index 5f389bd1ea4..5f389bd1ea4 100644
--- a/doc/.linting/vale/styles/gitlab/Contractions.yml
+++ b/doc/.vale/gitlab/Contractions.yml
diff --git a/doc/.linting/vale/styles/gitlab/FirstPerson.yml b/doc/.vale/gitlab/FirstPerson.yml
index 18c5265b0a6..18c5265b0a6 100644
--- a/doc/.linting/vale/styles/gitlab/FirstPerson.yml
+++ b/doc/.vale/gitlab/FirstPerson.yml
diff --git a/doc/.linting/vale/styles/gitlab/InternalLinkExtension.yml b/doc/.vale/gitlab/InternalLinkExtension.yml
index d07a2600798..d07a2600798 100644
--- a/doc/.linting/vale/styles/gitlab/InternalLinkExtension.yml
+++ b/doc/.vale/gitlab/InternalLinkExtension.yml
diff --git a/doc/.linting/vale/styles/gitlab/LatinTerms.yml b/doc/.vale/gitlab/LatinTerms.yml
index 8412631f8fe..8412631f8fe 100644
--- a/doc/.linting/vale/styles/gitlab/LatinTerms.yml
+++ b/doc/.vale/gitlab/LatinTerms.yml
diff --git a/doc/.linting/vale/styles/gitlab/OxfordComma.yml b/doc/.vale/gitlab/OxfordComma.yml
index 4b37ba8c2b9..4b37ba8c2b9 100644
--- a/doc/.linting/vale/styles/gitlab/OxfordComma.yml
+++ b/doc/.vale/gitlab/OxfordComma.yml
diff --git a/doc/.linting/vale/styles/gitlab/RelativeLinks.yml b/doc/.vale/gitlab/RelativeLinks.yml
index 95bd60dd6e4..95bd60dd6e4 100644
--- a/doc/.linting/vale/styles/gitlab/RelativeLinks.yml
+++ b/doc/.vale/gitlab/RelativeLinks.yml
diff --git a/doc/.linting/vale/styles/gitlab/SentenceSpacing.yml b/doc/.vale/gitlab/SentenceSpacing.yml
index b061f7f6f9e..b061f7f6f9e 100644
--- a/doc/.linting/vale/styles/gitlab/SentenceSpacing.yml
+++ b/doc/.vale/gitlab/SentenceSpacing.yml
diff --git a/doc/.linting/vale/styles/gitlab/Substitutions.yml b/doc/.vale/gitlab/Substitutions.yml
index b32a03e17d5..b32a03e17d5 100644
--- a/doc/.linting/vale/styles/gitlab/Substitutions.yml
+++ b/doc/.vale/gitlab/Substitutions.yml
diff --git a/doc/ci/review_apps/img/enable_review_app_v12_8.png b/doc/ci/review_apps/img/enable_review_app_v12_8.png
index 7d40f49725f..264e4834e72 100644
--- a/doc/ci/review_apps/img/enable_review_app_v12_8.png
+++ b/doc/ci/review_apps/img/enable_review_app_v12_8.png
Binary files differ
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index 4d84e921acf..46d1d4c2414 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -81,6 +81,8 @@ already reserved for category labels).
The descriptions on the [labels page](https://gitlab.com/groups/gitlab-org/-/labels)
explain what falls under each type label.
+The GitLab handbook documents [when something is a bug and when it is a feature request.](https://about.gitlab.com/handbook/product/product-management/process/feature-or-bug.html)
+
### Facet labels
Sometimes it's useful to refine the type of an issue. In those cases, you can
diff --git a/doc/development/event_tracking/backend.md b/doc/development/event_tracking/backend.md
new file mode 100644
index 00000000000..dc4d7279671
--- /dev/null
+++ b/doc/development/event_tracking/backend.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../../telemetry/backend.md'
+---
+
+This document was moved to [another location](../../telemetry/backend.md).
diff --git a/doc/development/event_tracking/frontend.md b/doc/development/event_tracking/frontend.md
new file mode 100644
index 00000000000..0e98daf15bb
--- /dev/null
+++ b/doc/development/event_tracking/frontend.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../../telemetry/frontend.md'
+---
+
+This document was moved to [another location](../../telemetry/frontend.md).
diff --git a/doc/development/event_tracking/index.md b/doc/development/event_tracking/index.md
new file mode 100644
index 00000000000..ae555e99c6b
--- /dev/null
+++ b/doc/development/event_tracking/index.md
@@ -0,0 +1,5 @@
+---
+redirect_to: '../../telemetry/index.md'
+---
+
+This document was moved to [another location](../../telemetry/index.md).
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index 1739c07ccd5..028e372985d 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -127,7 +127,7 @@ one major version. For example, it is safe to:
- `9.5.5` -> `9.5.9`
- `10.6.3` -> `10.6.6`
- `11.11.1` -> `11.11.8`
- - `12.0.4` -> `12.0.9`
+ - `12.0.4` -> `12.0.12`
- Upgrade the minor version:
- `8.9.4` -> `8.12.3`
- `9.2.3` -> `9.5.5`
@@ -144,9 +144,10 @@ It's also important to ensure that any background migrations have been fully com
before upgrading to a new major version. To see the current size of the `background_migration` queue,
[Check for background migrations before upgrading](../update/README.md#checking-for-background-migrations-before-upgrading).
-To ensure background migrations are successful, increment by one minor version during the version jump before installing newer releases.
+From version 12 onwards, an additional step is required. More significant migrations may occur during major release upgrades. To ensure these are successful, increment to the first minor version (`x.0.x`) during the major version jump. Then proceed with upgrading to a newer release.
+
+For example: `11.11.x` -> `12.0.x` -> `12.8.x`
-For example: `11.11.x` -> `12.0.x`
Please see the table below for some examples:
| Latest stable version | Your version | Recommended upgrade path | Note |
@@ -154,7 +155,8 @@ Please see the table below for some examples:
| 9.4.5 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.4.5` | `8.17.7` is the last version in version `8` |
| 10.1.4 | 8.13.4 | `8.13.4 -> 8.17.7 -> 9.5.10 -> 10.1.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9` |
| 11.3.4 | 8.13.4 | `8.13.4` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> `11.3.4` | `8.17.7` is the last version in version `8`, `9.5.10` is the last version in version `9`, `10.8.7` is the last version in version `10` |
-| 12.5.8 | 11.3.4 | `11.3.4` -> `11.11.8` -> `12.0.9` -> `12.5.8` | `11.11.8` is the last version in version `11` |
+| 12.5.8 | 11.3.4 | `11.3.4` -> `11.11.8` -> `12.0.12` -> `12.5.8` | `11.11.8` is the last version in version `11`. `12.0.x` [is a required step.](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/23211#note_272842444) |
+| 12.8.5 | 9.2.6 | `9.2.6` -> `9.5.10` -> `10.8.7` -> `11.11.8` -> `12.0.12` -> `12.8.5` | Four intermediate versions required: the final 9.5, 10.8, 11.11 releases, plus 12.0 |
More information about the release procedures can be found in our
[release documentation](https://gitlab.com/gitlab-org/release/docs). You may also want to read our
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index bbfcf53b3d4..c6c8256b4bb 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -40,6 +40,7 @@ stop_review:
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
+ dependencies: []
when: manual
allow_failure: true
only:
diff --git a/lib/gitlab/import_export/group/tree_restorer.rb b/lib/gitlab/import_export/group/tree_restorer.rb
index cbaa6929efa..247e39a68b9 100644
--- a/lib/gitlab/import_export/group/tree_restorer.rb
+++ b/lib/gitlab/import_export/group/tree_restorer.rb
@@ -17,9 +17,17 @@ module Gitlab
end
def restore
- @tree_hash = @group_hash || read_tree_hash
- @group_members = @tree_hash.delete('members')
- @children = @tree_hash.delete('children')
+ @relation_reader ||=
+ if @group_hash.present?
+ ImportExport::JSON::LegacyReader::User.new(@group_hash, reader.group_relation_names)
+ else
+ ImportExport::JSON::LegacyReader::File.new(@path, reader.group_relation_names)
+ end
+
+ @group_members = @relation_reader.consume_relation('members')
+ @children = @relation_reader.consume_attribute('children')
+ @relation_reader.consume_attribute('name')
+ @relation_reader.consume_attribute('path')
if members_mapper.map && restorer.restore
@children&.each do |group_hash|
@@ -45,21 +53,12 @@ module Gitlab
private
- def read_tree_hash
- json = IO.read(@path)
- ActiveSupport::JSON.decode(json)
- rescue => e
- @shared.error(e)
-
- raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
- end
-
def restorer
@relation_tree_restorer ||= RelationTreeRestorer.new(
user: @user,
shared: @shared,
importable: @group,
- tree_hash: @tree_hash.except('name', 'path'),
+ relation_reader: @relation_reader,
members_mapper: members_mapper,
object_builder: object_builder,
relation_factory: relation_factory,
diff --git a/lib/gitlab/import_export/json/legacy_reader.rb b/lib/gitlab/import_export/json/legacy_reader.rb
new file mode 100644
index 00000000000..477e41ae3eb
--- /dev/null
+++ b/lib/gitlab/import_export/json/legacy_reader.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module ImportExport
+ module JSON
+ class LegacyReader
+ class File < LegacyReader
+ def initialize(path, relation_names)
+ @path = path
+ super(relation_names)
+ end
+
+ def valid?
+ ::File.exist?(@path)
+ end
+
+ private
+
+ def tree_hash
+ @tree_hash ||= read_hash
+ end
+
+ def read_hash
+ ActiveSupport::JSON.decode(IO.read(@path))
+ rescue => e
+ Gitlab::ErrorTracking.log_exception(e)
+ raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
+ end
+ end
+
+ class User < LegacyReader
+ def initialize(tree_hash, relation_names)
+ @tree_hash = tree_hash
+ super(relation_names)
+ end
+
+ def valid?
+ @tree_hash.present?
+ end
+
+ protected
+
+ attr_reader :tree_hash
+ end
+
+ def initialize(relation_names)
+ @relation_names = relation_names.map(&:to_s)
+ end
+
+ def valid?
+ raise NotImplementedError
+ end
+
+ def legacy?
+ true
+ end
+
+ def root_attributes(excluded_attributes = [])
+ attributes.except(*excluded_attributes.map(&:to_s))
+ end
+
+ def consume_relation(key)
+ value = relations.delete(key)
+
+ return value unless block_given?
+
+ return if value.nil?
+
+ if value.is_a?(Array)
+ value.each.with_index do |item, idx|
+ yield(item, idx)
+ end
+ else
+ yield(value, 0)
+ end
+ end
+
+ def consume_attribute(key)
+ attributes.delete(key)
+ end
+
+ def sort_ci_pipelines_by_id
+ relations['ci_pipelines']&.sort_by! { |hash| hash['id'] }
+ end
+
+ private
+
+ attr_reader :relation_names
+
+ def tree_hash
+ raise NotImplementedError
+ end
+
+ def attributes
+ @attributes ||= tree_hash.slice!(*relation_names)
+ end
+
+ def relations
+ @relations ||= tree_hash.extract!(*relation_names)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/project/tree_loader.rb b/lib/gitlab/import_export/project/tree_loader.rb
deleted file mode 100644
index 6d4737a2d00..00000000000
--- a/lib/gitlab/import_export/project/tree_loader.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ImportExport
- module Project
- class TreeLoader
- def load(path, dedup_entries: false)
- tree_hash = ActiveSupport::JSON.decode(IO.read(path))
-
- if dedup_entries
- dedup_tree(tree_hash)
- else
- tree_hash
- end
- end
-
- private
-
- # This function removes duplicate entries from the given tree recursively
- # by caching nodes it encounters repeatedly. We only consider nodes for
- # which there can actually be multiple equivalent instances (e.g. strings,
- # hashes and arrays, but not `nil`s, numbers or booleans.)
- #
- # The algorithm uses a recursive depth-first descent with 3 cases, starting
- # with a root node (the tree/hash itself):
- # - a node has already been cached; in this case we return it from the cache
- # - a node has not been cached yet but should be; descend into its children
- # - a node is neither cached nor qualifies for caching; this is a no-op
- def dedup_tree(node, nodes_seen = {})
- if nodes_seen.key?(node) && distinguishable?(node)
- yield nodes_seen[node]
- elsif should_dedup?(node)
- nodes_seen[node] = node
-
- case node
- when Array
- node.each_index do |idx|
- dedup_tree(node[idx], nodes_seen) do |cached_node|
- node[idx] = cached_node
- end
- end
- when Hash
- node.each do |k, v|
- dedup_tree(v, nodes_seen) do |cached_node|
- node[k] = cached_node
- end
- end
- end
- else
- node
- end
- end
-
- # We do not need to consider nodes for which there cannot be multiple instances
- def should_dedup?(node)
- node && !(node.is_a?(Numeric) || node.is_a?(TrueClass) || node.is_a?(FalseClass))
- end
-
- # We can only safely de-dup values that are distinguishable. True value objects
- # are always distinguishable by nature. Hashes however can represent entities,
- # which are identified by ID, not value. We therefore disallow de-duping hashes
- # that do not have an `id` field, since we might risk dropping entities that
- # have equal attributes yet different identities.
- def distinguishable?(node)
- if node.is_a?(Hash)
- node.key?('id')
- else
- true
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export/project/tree_restorer.rb b/lib/gitlab/import_export/project/tree_restorer.rb
index 295e0d5f348..f8d25e14c02 100644
--- a/lib/gitlab/import_export/project/tree_restorer.rb
+++ b/lib/gitlab/import_export/project/tree_restorer.rb
@@ -4,8 +4,6 @@ module Gitlab
module ImportExport
module Project
class TreeRestorer
- LARGE_PROJECT_FILE_SIZE_BYTES = 500.megabyte
-
attr_reader :user
attr_reader :shared
attr_reader :project
@@ -14,12 +12,12 @@ module Gitlab
@user = user
@shared = shared
@project = project
- @tree_loader = TreeLoader.new
end
def restore
- @tree_hash = read_tree_hash
- @project_members = @tree_hash.delete('project_members')
+ @relation_reader = ImportExport::JSON::LegacyReader::File.new(File.join(shared.export_path, 'project.json'), reader.project_relation_names)
+
+ @project_members = @relation_reader.consume_relation('project_members')
if relation_tree_restorer.restore
import_failure_service.with_retry(action: 'set_latest_merge_request_diff_ids!') do
@@ -37,24 +35,12 @@ module Gitlab
private
- def large_project?(path)
- File.size(path) >= LARGE_PROJECT_FILE_SIZE_BYTES
- end
-
- def read_tree_hash
- path = File.join(@shared.export_path, 'project.json')
- @tree_loader.load(path, dedup_entries: large_project?(path))
- rescue => e
- Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
- raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
- end
-
def relation_tree_restorer
@relation_tree_restorer ||= RelationTreeRestorer.new(
user: @user,
shared: @shared,
importable: @project,
- tree_hash: @tree_hash,
+ relation_reader: @relation_reader,
object_builder: object_builder,
members_mapper: members_mapper,
relation_factory: relation_factory,
diff --git a/lib/gitlab/import_export/reader.rb b/lib/gitlab/import_export/reader.rb
index 1390770acef..8d36d05ca6f 100644
--- a/lib/gitlab/import_export/reader.rb
+++ b/lib/gitlab/import_export/reader.rb
@@ -17,10 +17,18 @@ module Gitlab
tree_by_key(:project)
end
+ def project_relation_names
+ attributes_finder.find_relations_tree(:project).keys
+ end
+
def group_tree
tree_by_key(:group)
end
+ def group_relation_names
+ attributes_finder.find_relations_tree(:group).keys
+ end
+
def group_members_tree
tree_by_key(:group_members)
end
diff --git a/lib/gitlab/import_export/relation_tree_restorer.rb b/lib/gitlab/import_export/relation_tree_restorer.rb
index 8359eefc846..466cb03862e 100644
--- a/lib/gitlab/import_export/relation_tree_restorer.rb
+++ b/lib/gitlab/import_export/relation_tree_restorer.rb
@@ -9,13 +9,13 @@ module Gitlab
attr_reader :user
attr_reader :shared
attr_reader :importable
- attr_reader :tree_hash
+ attr_reader :relation_reader
- def initialize(user:, shared:, importable:, tree_hash:, members_mapper:, object_builder:, relation_factory:, reader:)
+ def initialize(user:, shared:, importable:, relation_reader:, members_mapper:, object_builder:, relation_factory:, reader:)
@user = user
@shared = shared
@importable = importable
- @tree_hash = tree_hash
+ @relation_reader = relation_reader
@members_mapper = members_mapper
@object_builder = object_builder
@relation_factory = relation_factory
@@ -30,7 +30,7 @@ module Gitlab
bulk_inserts_enabled = @importable.class == ::Project &&
Feature.enabled?(:import_bulk_inserts, @importable.group)
BulkInsertableAssociations.with_bulk_insert(enabled: bulk_inserts_enabled) do
- update_relation_hashes!
+ fix_ci_pipelines_not_sorted_on_legacy_project_json!
create_relations!
end
end
@@ -57,18 +57,8 @@ module Gitlab
end
def process_relation!(relation_key, relation_definition)
- data_hashes = @tree_hash.delete(relation_key)
- return unless data_hashes
-
- # we do not care if we process array or hash
- data_hashes = [data_hashes] unless data_hashes.is_a?(Array)
-
- relation_index = 0
-
- # consume and remove objects from memory
- while data_hash = data_hashes.shift
+ @relation_reader.consume_relation(relation_key) do |data_hash, relation_index|
process_relation_item!(relation_key, relation_definition, relation_index, data_hash)
- relation_index += 1
end
end
@@ -103,10 +93,7 @@ module Gitlab
end
def update_params!
- params = @tree_hash.reject do |key, _|
- relations.include?(key)
- end
-
+ params = @relation_reader.root_attributes(relations.keys)
params = params.merge(present_override_params)
# Cleaning all imported and overridden params
@@ -223,8 +210,13 @@ module Gitlab
}
end
- def update_relation_hashes!
- @tree_hash['ci_pipelines']&.sort_by! { |hash| hash['id'] }
+ # Temporary fix for https://gitlab.com/gitlab-org/gitlab/-/issues/27883 when import from legacy project.json
+ # This should be removed once legacy JSON format is deprecated.
+ # Ndjson export file will fix the order during project export.
+ def fix_ci_pipelines_not_sorted_on_legacy_project_json!
+ return unless relation_reader.legacy?
+
+ relation_reader.sort_ci_pipelines_by_id
end
end
end
diff --git a/lib/gitlab/import_export/relation_tree_saver.rb b/lib/gitlab/import_export/relation_tree_saver.rb
index a0452071ccf..ed5392c13d0 100644
--- a/lib/gitlab/import_export/relation_tree_saver.rb
+++ b/lib/gitlab/import_export/relation_tree_saver.rb
@@ -18,7 +18,7 @@ module Gitlab
def save(tree, dir_path, filename)
mkdir_p(dir_path)
- tree_json = JSON.generate(tree)
+ tree_json = ::JSON.generate(tree)
File.write(File.join(dir_path, filename), tree_json)
end
diff --git a/spec/fixtures/lib/gitlab/import_export/invalid_json/project.json b/spec/fixtures/lib/gitlab/import_export/invalid_json/project.json
new file mode 100644
index 00000000000..83cb34eea91
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/import_export/invalid_json/project.json
@@ -0,0 +1,3 @@
+{
+ "invalid" json
+}
diff --git a/spec/frontend/__mocks__/mousetrap/index.js b/spec/frontend/__mocks__/mousetrap/index.js
new file mode 100644
index 00000000000..63c92fa9a09
--- /dev/null
+++ b/spec/frontend/__mocks__/mousetrap/index.js
@@ -0,0 +1,6 @@
+/* global Mousetrap */
+// `mousetrap` uses amd which webpack understands but Jest does not
+// Thankfully it also writes to a global export so we can es6-ify it
+import 'mousetrap';
+
+export default Mousetrap;
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 5f97182489e..15f91871437 100644
--- a/spec/javascripts/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -1,6 +1,7 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'spec/test_constants';
import Mousetrap from 'mousetrap';
import App from '~/diffs/components/app.vue';
@@ -12,14 +13,17 @@ import CommitWidget from '~/diffs/components/commit_widget.vue';
import TreeList from '~/diffs/components/tree_list.vue';
import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
import createDiffsStore from '../create_diffs_store';
+import axios from '~/lib/utils/axios_utils';
import diffsMockData from '../mock_data/merge_request_diffs';
const mergeRequestDiff = { version_index: 1 };
+const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
describe('diffs/components/app', () => {
const oldMrTabs = window.mrTabs;
let store;
let wrapper;
+ let mock;
function createComponent(props = {}, extendStore = () => {}) {
const localVue = createLocalVue();
@@ -34,7 +38,7 @@ describe('diffs/components/app', () => {
wrapper = shallowMount(localVue.extend(App), {
localVue,
propsData: {
- endpoint: `${TEST_HOST}/diff/endpoint`,
+ endpoint: TEST_ENDPOINT,
endpointMetadata: `${TEST_HOST}/diff/endpointMetadata`,
endpointBatch: `${TEST_HOST}/diff/endpointBatch`,
projectPath: 'namespace/project',
@@ -61,8 +65,12 @@ describe('diffs/components/app', () => {
beforeEach(() => {
// setup globals (needed for component to mount :/)
- window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
- window.mrTabs.expandViewContainer = jasmine.createSpy();
+ window.mrTabs = {
+ resetViewContainer: jest.fn(),
+ };
+ window.mrTabs.expandViewContainer = jest.fn();
+ mock = new MockAdapter(axios);
+ mock.onGet(TEST_ENDPOINT).reply(200, {});
});
afterEach(() => {
@@ -71,6 +79,8 @@ describe('diffs/components/app', () => {
// reset component
wrapper.destroy();
+
+ mock.restore();
});
describe('fetch diff methods', () => {
@@ -80,15 +90,15 @@ describe('diffs/components/app', () => {
store.state.notes.discussions = 'test';
return Promise.resolve({ real_size: 100 });
};
- spyOn(window, 'requestIdleCallback').and.callFake(fn => fn());
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(fn => fn());
createComponent();
- spyOn(wrapper.vm, 'fetchDiffFiles').and.callFake(fetchResolver);
- spyOn(wrapper.vm, 'fetchDiffFilesMeta').and.callFake(fetchResolver);
- spyOn(wrapper.vm, 'fetchDiffFilesBatch').and.callFake(fetchResolver);
- spyOn(wrapper.vm, 'setDiscussions');
- spyOn(wrapper.vm, 'startRenderDiffsQueue');
- spyOn(wrapper.vm, 'unwatchDiscussions');
- spyOn(wrapper.vm, 'unwatchRetrievingBatches');
+ jest.spyOn(wrapper.vm, 'fetchDiffFiles').mockImplementation(fetchResolver);
+ jest.spyOn(wrapper.vm, 'fetchDiffFilesMeta').mockImplementation(fetchResolver);
+ jest.spyOn(wrapper.vm, 'fetchDiffFilesBatch').mockImplementation(fetchResolver);
+ jest.spyOn(wrapper.vm, 'setDiscussions').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'startRenderDiffsQueue').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'unwatchDiscussions').mockImplementation(() => {});
+ jest.spyOn(wrapper.vm, 'unwatchRetrievingBatches').mockImplementation(() => {});
store.state.diffs.retrievingBatches = true;
store.state.diffs.diffFiles = [];
wrapper.vm.$nextTick(done);
@@ -236,7 +246,7 @@ describe('diffs/components/app', () => {
wrapper.vm.fetchData(false);
expect(wrapper.vm.fetchDiffFiles).toHaveBeenCalled();
- setTimeout(() => {
+ setImmediate(() => {
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
@@ -255,7 +265,7 @@ describe('diffs/components/app', () => {
wrapper.vm.fetchData(false);
expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
- setTimeout(() => {
+ setImmediate(() => {
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
@@ -272,7 +282,7 @@ describe('diffs/components/app', () => {
wrapper.vm.fetchData(false);
expect(wrapper.vm.fetchDiffFiles).not.toHaveBeenCalled();
- setTimeout(() => {
+ setImmediate(() => {
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
@@ -350,23 +360,21 @@ describe('diffs/components/app', () => {
});
// Component uses $nextTick so we wait until that has finished
- setTimeout(() => {
+ setImmediate(() => {
expect(store.state.diffs.highlightedRow).toBe('ABC_123');
done();
});
});
- it('marks current diff file based on currently highlighted row', done => {
+ it('marks current diff file based on currently highlighted row', () => {
createComponent({
shouldShow: true,
});
// Component uses $nextTick so we wait until that has finished
- setTimeout(() => {
+ return wrapper.vm.$nextTick().then(() => {
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
-
- done();
});
});
});
@@ -403,7 +411,7 @@ describe('diffs/components/app', () => {
});
// Component uses $nextTick so we wait until that has finished
- setTimeout(() => {
+ setImmediate(() => {
expect(store.state.diffs.currentDiffFileId).toBe('ABC');
done();
@@ -449,7 +457,7 @@ describe('diffs/components/app', () => {
describe('visible app', () => {
beforeEach(() => {
- spy = jasmine.createSpy('spy');
+ spy = jest.fn();
createComponent({
shouldShow: true,
@@ -459,21 +467,18 @@ describe('diffs/components/app', () => {
});
});
- it('calls `jumpToFile()` with correct parameter whenever pre-defined key is pressed', done => {
- wrapper.vm
- .$nextTick()
- .then(() => {
- Object.keys(mappings).forEach(function(key) {
- Mousetrap.trigger(key);
+ it.each(Object.keys(mappings))(
+ 'calls `jumpToFile()` with correct parameter whenever pre-defined %s is pressed',
+ key => {
+ return wrapper.vm.$nextTick().then(() => {
+ expect(spy).not.toHaveBeenCalled();
- expect(spy.calls.mostRecent().args).toEqual([mappings[key]]);
- });
+ Mousetrap.trigger(key);
- expect(spy.calls.count()).toEqual(Object.keys(mappings).length);
- })
- .then(done)
- .catch(done.fail);
- });
+ expect(spy).toHaveBeenCalledWith(mappings[key]);
+ });
+ },
+ );
it('does not call `jumpToFile()` when unknown key is pressed', done => {
wrapper.vm
@@ -490,7 +495,7 @@ describe('diffs/components/app', () => {
describe('hideen app', () => {
beforeEach(() => {
- spy = jasmine.createSpy('spy');
+ spy = jest.fn();
createComponent({
shouldShow: false,
@@ -504,7 +509,7 @@ describe('diffs/components/app', () => {
wrapper.vm
.$nextTick()
.then(() => {
- Object.keys(mappings).forEach(function(key) {
+ Object.keys(mappings).forEach(key => {
Mousetrap.trigger(key);
expect(spy).not.toHaveBeenCalled();
@@ -520,7 +525,7 @@ describe('diffs/components/app', () => {
let spy;
beforeEach(() => {
- spy = jasmine.createSpy();
+ spy = jest.fn();
createComponent({}, () => {
store.state.diffs.diffFiles = [
@@ -545,15 +550,15 @@ describe('diffs/components/app', () => {
.then(() => {
wrapper.vm.jumpToFile(+1);
- expect(spy.calls.mostRecent().args).toEqual(['222.js']);
+ expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['222.js']);
store.state.diffs.currentDiffFileId = '222';
wrapper.vm.jumpToFile(+1);
- expect(spy.calls.mostRecent().args).toEqual(['333.js']);
+ expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['333.js']);
store.state.diffs.currentDiffFileId = '333';
wrapper.vm.jumpToFile(-1);
- expect(spy.calls.mostRecent().args).toEqual(['222.js']);
+ expect(spy.mock.calls[spy.mock.calls.length - 1]).toEqual(['222.js']);
})
.then(done)
.catch(done.fail);
@@ -602,7 +607,7 @@ describe('diffs/components/app', () => {
expect(wrapper.contains(CompareVersions)).toBe(true);
expect(wrapper.find(CompareVersions).props()).toEqual(
- jasmine.objectContaining({
+ expect.objectContaining({
targetBranch: {
branchName: 'target-branch',
versionIndex: -1,
@@ -625,7 +630,7 @@ describe('diffs/components/app', () => {
expect(wrapper.contains(HiddenFilesWarning)).toBe(true);
expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
- jasmine.objectContaining({
+ expect.objectContaining({
total: '5',
plainDiffPath: 'plain diff path',
emailPatchPath: 'email patch path',
@@ -663,7 +668,7 @@ describe('diffs/components/app', () => {
let toggleShowTreeList;
beforeEach(() => {
- toggleShowTreeList = jasmine.createSpy('toggleShowTreeList');
+ toggleShowTreeList = jest.fn();
});
afterEach(() => {
diff --git a/spec/frontend/diffs/create_diffs_store.js b/spec/frontend/diffs/create_diffs_store.js
new file mode 100644
index 00000000000..aacde99964c
--- /dev/null
+++ b/spec/frontend/diffs/create_diffs_store.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import diffsModule from '~/diffs/store/modules';
+import notesModule from '~/notes/stores/modules';
+
+Vue.use(Vuex);
+
+export default function createDiffsStore() {
+ return new Vuex.Store({
+ modules: {
+ diffs: diffsModule(),
+ notes: notesModule(),
+ },
+ });
+}
diff --git a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
index 177cd4559ca..efa58a4a47b 100644
--- a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
+++ b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap
@@ -8,6 +8,7 @@ exports[`IDE pipelines list when loaded renders empty state when no latestPipeli
<empty-state-stub
cansetci="true"
+ class="mb-auto mt-auto"
emptystatesvgpath="http://test.host"
helppagepath="http://test.host"
/>
diff --git a/spec/frontend/lib/utils/icon_utils_spec.js b/spec/frontend/lib/utils/icon_utils_spec.js
index 816d634ad15..f798dc6744d 100644
--- a/spec/frontend/lib/utils/icon_utils_spec.js
+++ b/spec/frontend/lib/utils/icon_utils_spec.js
@@ -1,10 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import * as iconUtils from '~/lib/utils/icon_utils';
+import { clearSvgIconPathContentCache, getSvgIconPathContent } from '~/lib/utils/icon_utils';
describe('Icon utils', () => {
describe('getSvgIconPathContent', () => {
let spriteIcons;
+ let axiosMock;
+ const mockName = 'mockIconName';
+ const mockPath = 'mockPath';
+ const mockIcons = `<svg><symbol id="${mockName}"><path d="${mockPath}"/></symbol></svg>`;
beforeAll(() => {
spriteIcons = gon.sprite_icons;
@@ -15,45 +19,63 @@ describe('Icon utils', () => {
gon.sprite_icons = spriteIcons;
});
- let axiosMock;
- let mockEndpoint;
- const mockName = 'mockIconName';
- const mockPath = 'mockPath';
- const getIcon = () => iconUtils.getSvgIconPathContent(mockName);
-
beforeEach(() => {
axiosMock = new MockAdapter(axios);
- mockEndpoint = axiosMock.onGet(gon.sprite_icons);
});
afterEach(() => {
axiosMock.restore();
+ clearSvgIconPathContentCache();
});
- it('extracts svg icon path content from sprite icons', () => {
- mockEndpoint.replyOnce(
- 200,
- `<svg><symbol id="${mockName}"><path d="${mockPath}"/></symbol></svg>`,
- );
-
- return getIcon().then(path => {
- expect(path).toBe(mockPath);
+ describe('when the icons can be loaded', () => {
+ beforeEach(() => {
+ axiosMock.onGet(gon.sprite_icons).reply(200, mockIcons);
});
- });
- it('returns null if icon path content does not exist', () => {
- mockEndpoint.replyOnce(200, ``);
+ it('extracts svg icon path content from sprite icons', () => {
+ return getSvgIconPathContent(mockName).then(path => {
+ expect(path).toBe(mockPath);
+ });
+ });
- return getIcon().then(path => {
- expect(path).toBe(null);
+ it('returns null if icon path content does not exist', () => {
+ return getSvgIconPathContent('missing-icon').then(path => {
+ expect(path).toBe(null);
+ });
});
});
- it('returns null if an http error occurs', () => {
- mockEndpoint.replyOnce(500);
+ describe('when the icons cannot be loaded on the first 2 tries', () => {
+ beforeEach(() => {
+ axiosMock
+ .onGet(gon.sprite_icons)
+ .replyOnce(500)
+ .onGet(gon.sprite_icons)
+ .replyOnce(500)
+ .onGet(gon.sprite_icons)
+ .reply(200, mockIcons);
+ });
+
+ it('returns null', () => {
+ return getSvgIconPathContent(mockName).then(path => {
+ expect(path).toBe(null);
+ });
+ });
- return getIcon().then(path => {
- expect(path).toBe(null);
+ it('extracts svg icon path content, after 2 attempts', () => {
+ return getSvgIconPathContent(mockName)
+ .then(path1 => {
+ expect(path1).toBe(null);
+ return getSvgIconPathContent(mockName);
+ })
+ .then(path2 => {
+ expect(path2).toBe(null);
+ return getSvgIconPathContent(mockName);
+ })
+ .then(path3 => {
+ expect(path3).toBe(mockPath);
+ });
});
});
});
diff --git a/spec/frontend/mocks/ce/diffs/workers/tree_worker.js b/spec/frontend/mocks/ce/diffs/workers/tree_worker.js
new file mode 100644
index 00000000000..a33ddbbfe63
--- /dev/null
+++ b/spec/frontend/mocks/ce/diffs/workers/tree_worker.js
@@ -0,0 +1,8 @@
+/* eslint-disable class-methods-use-this */
+export default class TreeWorkerMock {
+ addEventListener() {}
+
+ terminate() {}
+
+ postMessage() {}
+}
diff --git a/spec/javascripts/diffs/create_diffs_store.js b/spec/javascripts/diffs/create_diffs_store.js
index aacde99964c..cfefd4238b8 100644
--- a/spec/javascripts/diffs/create_diffs_store.js
+++ b/spec/javascripts/diffs/create_diffs_store.js
@@ -1,15 +1 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import diffsModule from '~/diffs/store/modules';
-import notesModule from '~/notes/stores/modules';
-
-Vue.use(Vuex);
-
-export default function createDiffsStore() {
- return new Vuex.Store({
- modules: {
- diffs: diffsModule(),
- notes: notesModule(),
- },
- });
-}
+export { default } from '../../frontend/diffs/create_diffs_store';
diff --git a/spec/lib/gitlab/import_export/json/legacy_reader_spec.rb b/spec/lib/gitlab/import_export/json/legacy_reader_spec.rb
new file mode 100644
index 00000000000..0009a5f81de
--- /dev/null
+++ b/spec/lib/gitlab/import_export/json/legacy_reader_spec.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::ImportExport::JSON::LegacyReader::User do
+ let(:relation_names) { [] }
+ let(:legacy_reader) { described_class.new(tree_hash, relation_names) }
+
+ describe '#valid?' do
+ subject { legacy_reader.valid? }
+
+ context 'tree_hash not present' do
+ let(:tree_hash) { nil }
+
+ it { is_expected.to be false }
+ end
+
+ context 'tree_hash presents' do
+ let(:tree_hash) { { "issues": [] } }
+
+ it { is_expected.to be true }
+ end
+ end
+end
+
+describe Gitlab::ImportExport::JSON::LegacyReader::File do
+ let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/light/project.json' }
+ let(:project_tree) { JSON.parse(File.read(fixture)) }
+ let(:relation_names) { [] }
+ let(:legacy_reader) { described_class.new(path, relation_names) }
+
+ describe '#valid?' do
+ subject { legacy_reader.valid? }
+
+ context 'given valid path' do
+ let(:path) { fixture }
+
+ it { is_expected.to be true }
+ end
+
+ context 'given invalid path' do
+ let(:path) { 'spec/non-existing-folder/do-not-create-this-file.json' }
+
+ it { is_expected.to be false }
+ end
+ end
+
+ describe '#root_attributes' do
+ let(:path) { fixture }
+
+ subject { legacy_reader.root_attributes(excluded_attributes) }
+
+ context 'No excluded attributes' do
+ let(:excluded_attributes) { [] }
+ let(:relation_names) { [] }
+
+ it 'returns the whole tree from parsed JSON' do
+ expect(subject).to eq(project_tree)
+ end
+ end
+
+ context 'Some attributes are excluded' do
+ let(:excluded_attributes) { %w[milestones labels issues services snippets] }
+ let(:relation_names) { %w[import_type archived] }
+
+ it 'returns hash without excluded attributes and relations' do
+ expect(subject).not_to include('milestones', 'labels', 'issues', 'services', 'snippets', 'import_type', 'archived')
+ end
+ end
+ end
+
+ describe '#consume_relation' do
+ let(:path) { fixture }
+ let(:key) { 'description' }
+
+ context 'block not given' do
+ it 'returns value of the key' do
+ expect(legacy_reader).to receive(:relations).and_return({ key => 'test value' })
+ expect(legacy_reader.consume_relation(key)).to eq('test value')
+ end
+ end
+
+ context 'key has been consumed' do
+ before do
+ legacy_reader.consume_relation(key)
+ end
+
+ it 'does not yield' do
+ expect do |blk|
+ legacy_reader.consume_relation(key, &blk)
+ end.not_to yield_control
+ end
+ end
+
+ context 'value is nil' do
+ before do
+ expect(legacy_reader).to receive(:relations).and_return({ key => nil })
+ end
+
+ it 'does not yield' do
+ expect do |blk|
+ legacy_reader.consume_relation(key, &blk)
+ end.not_to yield_control
+ end
+ end
+
+ context 'value is not array' do
+ before do
+ expect(legacy_reader).to receive(:relations).and_return({ key => 'value' })
+ end
+
+ it 'yield the value with index 0' do
+ expect do |blk|
+ legacy_reader.consume_relation(key, &blk)
+ end.to yield_with_args('value', 0)
+ end
+ end
+
+ context 'value is an array' do
+ before do
+ expect(legacy_reader).to receive(:relations).and_return({ key => %w[item1 item2 item3] })
+ end
+
+ it 'yield each array element with index' do
+ expect do |blk|
+ legacy_reader.consume_relation(key, &blk)
+ end.to yield_successive_args(['item1', 0], ['item2', 1], ['item3', 2])
+ end
+ end
+ end
+
+ describe '#tree_hash' do
+ let(:path) { fixture }
+
+ subject { legacy_reader.send(:tree_hash) }
+
+ it 'parses the JSON into the expected tree' do
+ expect(subject).to eq(project_tree)
+ end
+
+ context 'invalid JSON' do
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/invalid_json/project.json' }
+
+ it 'raise Exception' do
+ expect { subject }.to raise_exception(Gitlab::ImportExport::Error, 'Incorrect JSON format')
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project/tree_loader_spec.rb b/spec/lib/gitlab/import_export/project/tree_loader_spec.rb
deleted file mode 100644
index e683eefa7c0..00000000000
--- a/spec/lib/gitlab/import_export/project/tree_loader_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::ImportExport::Project::TreeLoader do
- let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/with_duplicates.json' }
- let(:project_tree) { JSON.parse(File.read(fixture)) }
-
- context 'without de-duplicating entries' do
- let(:parsed_tree) do
- subject.load(fixture)
- end
-
- it 'parses the JSON into the expected tree' do
- expect(parsed_tree).to eq(project_tree)
- end
-
- it 'does not de-duplicate entries' do
- expect(parsed_tree['duped_hash_with_id']).not_to be(parsed_tree['array'][0]['duped_hash_with_id'])
- end
- end
-
- context 'with de-duplicating entries' do
- let(:parsed_tree) do
- subject.load(fixture, dedup_entries: true)
- end
-
- it 'parses the JSON into the expected tree' do
- expect(parsed_tree).to eq(project_tree)
- end
-
- it 'de-duplicates equal values' do
- expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['array'][0]['duped_hash_with_id'])
- expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['nested']['duped_hash_with_id'])
- expect(parsed_tree['duped_array']).to be(parsed_tree['array'][1]['duped_array'])
- expect(parsed_tree['duped_array']).to be(parsed_tree['nested']['duped_array'])
- end
-
- it 'does not de-duplicate hashes without IDs' do
- expect(parsed_tree['duped_hash_no_id']).to eq(parsed_tree['array'][2]['duped_hash_no_id'])
- expect(parsed_tree['duped_hash_no_id']).not_to be(parsed_tree['array'][2]['duped_hash_no_id'])
- end
-
- it 'keeps single entries intact' do
- expect(parsed_tree['simple']).to eq(42)
- expect(parsed_tree['nested']['array']).to eq(["don't touch"])
- end
- end
-end
diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
index 9c2b202d5bb..e38ef75d085 100644
--- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb
@@ -783,7 +783,8 @@ describe Gitlab::ImportExport::Project::TreeRestorer do
end
before do
- expect(restorer).to receive(:read_tree_hash) { tree_hash }
+ allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:valid?).and_return(true)
+ allow_any_instance_of(Gitlab::ImportExport::JSON::LegacyReader::File).to receive(:tree_hash) { tree_hash }
end
context 'no group visibility' do
diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
index 80901feb893..578418998c0 100644
--- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# This spec is a lightweight version of:
-# * project_tree_restorer_spec.rb
+# * project/tree_restorer_spec.rb
#
# In depth testing is being done in the above specs.
# This spec tests that restore project works
@@ -25,7 +25,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
described_class.new(
user: user,
shared: shared,
- tree_hash: tree_hash,
+ relation_reader: relation_reader,
importable: importable,
object_builder: object_builder,
members_mapper: members_mapper,
@@ -36,14 +36,7 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
subject { relation_tree_restorer.restore }
- context 'when restoring a project' do
- let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
- let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
- let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
- let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
- let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
- let(:tree_hash) { importable_hash }
-
+ shared_examples 'import project successfully' do
it 'restores project tree' do
expect(subject).to eq(true)
end
@@ -66,4 +59,18 @@ describe Gitlab::ImportExport::RelationTreeRestorer do
end
end
end
+
+ context 'when restoring a project' do
+ let(:path) { 'spec/fixtures/lib/gitlab/import_export/complex/project.json' }
+ let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') }
+ let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder }
+ let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory }
+ let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) }
+
+ context 'using legacy reader' do
+ let(:relation_reader) { Gitlab::ImportExport::JSON::LegacyReader::File.new(path, reader.project_relation_names) }
+
+ it_behaves_like 'import project successfully'
+ end
+ end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 3df620d1fea..5b87ec022ae 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -625,4 +625,14 @@ describe SystemNoteService do
described_class.discussion_lock(issuable, double)
end
end
+
+ describe '.auto_resolve_prometheus_alert' do
+ it 'calls IssuableService' do
+ expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
+ expect(service).to receive(:auto_resolve_prometheus_alert)
+ end
+
+ described_class.auto_resolve_prometheus_alert(noteable, project, author)
+ end
+ end
end
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index c0aaa65971a..477f9eae39e 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -654,4 +654,16 @@ describe ::SystemNotes::IssuablesService do
.to eq('resolved the corresponding error and closed the issue.')
end
end
+
+ describe '#auto_resolve_prometheus_alert' do
+ subject { service.auto_resolve_prometheus_alert }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'closed' }
+ end
+
+ it 'creates the expected system note' do
+ expect(subject.note).to eq('automatically closed this issue because the alert resolved.')
+ end
+ end
end