summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-05-17 12:08:06 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-05-17 12:08:06 +0000
commitf0a387b4a5f08d6739894455664b4d3cb1509cc6 (patch)
tree3c5b144fbb894189dfb9a61e47c53c54af4415f4
parent91c2554bcf93c3c41aa830da4dd7a2d4b7483e2d (diff)
downloadgitlab-ce-f0a387b4a5f08d6739894455664b4d3cb1509cc6.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue45
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue85
-rw-r--r--app/assets/javascripts/issues/show/utils.js99
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue4
-rw-r--r--app/assets/javascripts/jobs/components/stuck_block.vue4
-rw-r--r--app/assets/javascripts/jobs/store/getters.js2
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js4
-rw-r--r--app/assets/javascripts/sortable/constants.js4
-rw-r--r--app/assets/javascripts/sortable/utils.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue19
-rw-r--r--app/assets/stylesheets/errors.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/issues_show.scss47
-rw-r--r--app/controllers/pwa_controller.rb12
-rw-r--r--app/helpers/merge_requests_helper.rb4
-rw-r--r--app/views/pwa/offline.html.haml31
-rw-r--r--app/views/shared/notes/_hints.html.haml11
-rw-r--r--config/feature_flags/development/automated_email_provision.yml2
-rw-r--r--config/routes.rb2
-rw-r--r--data/removals/15_0/15-0-secret-detection-configurations.yml30
-rw-r--r--doc/development/snowplow/troubleshooting.md24
-rw-r--r--doc/update/removals.md18
-rw-r--r--doc/user/application_security/container_scanning/index.md4
-rw-r--r--doc/user/application_security/secret_detection/index.md11
-rw-r--r--doc/user/free_user_limit.md60
-rw-r--r--doc/user/project/issues/managing_issues.md10
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schema_validator.rb1
-rw-r--r--locale/gitlab.pot27
-rw-r--r--qa/qa/page/component/blob_content.rb11
-rw-r--r--qa/qa/page/file/form.rb3
-rw-r--r--qa/qa/page/main/login.rb9
-rw-r--r--spec/frontend/issues/show/components/app_spec.js11
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js17
-rw-r--r--spec/frontend/issues/show/utils_spec.js40
-rw-r--r--spec/frontend/jobs/components/stuck_block_spec.js6
-rw-r--r--spec/frontend/jobs/store/getters_spec.js8
-rw-r--r--spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb238
-rw-r--r--spec/requests/pwa_controller_spec.rb14
-rw-r--r--spec/views/shared/notes/_form.html.haml_spec.rb2
-rw-r--r--workhorse/internal/redis/keywatcher.go5
-rw-r--r--workhorse/internal/redis/keywatcher_test.go227
41 files changed, 930 insertions, 241 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 235e115267d..a149f31c093 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-42fab8fc526215f9426bc9f459f9e6da0951c574
+6b31501b13eae70aea5061edc8273c551ba4c349
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index e7051cec956..5c4aae45ef2 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -342,20 +342,24 @@ export default {
});
},
- updateFormState(state) {
+ setFormState(state) {
this.store.setFormState(state);
},
- updateAndShowForm(templates = {}) {
+ updateFormState(templates = {}) {
+ this.setFormState({
+ title: this.state.titleText,
+ description: this.state.descriptionText,
+ lock_version: this.state.lock_version,
+ lockedWarningVisible: false,
+ updateLoading: false,
+ issuableTemplates: templates,
+ });
+ },
+
+ updateAndShowForm(templates) {
if (!this.showForm) {
- this.store.setFormState({
- title: this.state.titleText,
- description: this.state.descriptionText,
- lock_version: this.state.lock_version,
- lockedWarningVisible: false,
- updateLoading: false,
- issuableTemplates: templates,
- });
+ this.updateFormState(templates);
this.showForm = true;
}
},
@@ -388,9 +392,7 @@ export default {
},
updateIssuable() {
- this.store.setFormState({
- updateLoading: true,
- });
+ this.setFormState({ updateLoading: true });
const {
store: { formState },
@@ -428,10 +430,6 @@ export default {
.catch((error = {}) => {
const { message, response = {} } = error;
- this.store.setFormState({
- updateLoading: false,
- });
-
let errMsg = this.defaultErrorMessage;
if (response.data && response.data.errors) {
@@ -443,6 +441,9 @@ export default {
this.flashContainer = createFlash({
message: errMsg,
});
+ })
+ .finally(() => {
+ this.setFormState({ updateLoading: false });
});
},
@@ -461,6 +462,12 @@ export default {
}
},
+ handleListItemReorder(description) {
+ this.updateFormState();
+ this.setFormState({ description });
+ this.updateIssuable();
+ },
+
taskListUpdateStarted() {
this.poll.stop();
},
@@ -498,7 +505,7 @@ export default {
:can-attach-file="canAttachFile"
:enable-autocomplete="enableAutocomplete"
:issuable-type="issuableType"
- @updateForm="updateFormState"
+ @updateForm="setFormState"
/>
</div>
<div v-else>
@@ -573,6 +580,8 @@ export default {
:issuable-type="issuableType"
:update-url="updateEndpoint"
:lock-version="state.lock_version"
+ :is-updating="formState.updateLoading"
+ @listItemReorder="handleListItemReorder"
@taskListUpdateStarted="taskListUpdateStarted"
@taskListUpdateSucceeded="taskListUpdateSucceeded"
@taskListUpdateFailed="taskListUpdateFailed"
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 831cef66836..4f97458dcd1 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -7,6 +7,7 @@ import {
GlModalDirective,
} from '@gitlab/ui';
import $ from 'jquery';
+import Sortable from 'sortablejs';
import Vue from 'vue';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
@@ -14,6 +15,7 @@ import createFlash from '~/flash';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { getParameterByName, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
+import { getSortableDefaultOptions, isDragging } from '~/sortable/utils';
import TaskList from '~/task_list';
import Tracking from '~/tracking';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
@@ -22,9 +24,14 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import animateMixin from '../mixins/animate';
+import { convertDescriptionWithNewSort } from '../utils';
Vue.use(GlToast);
+const workItemTypes = {
+ TASK: 'task',
+};
+
export default {
directives: {
SafeHtml,
@@ -76,6 +83,11 @@ export default {
required: false,
default: null,
},
+ isUpdating: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
const workItemId = getParameterByName('work_item_id');
@@ -146,6 +158,9 @@ export default {
this.openWorkItemDetailModal(taskLink);
}
},
+ beforeDestroy() {
+ this.removeAllPointerEventListeners();
+ },
methods: {
renderGFM() {
$(this.$refs['gfm-content']).renderGFM();
@@ -161,9 +176,67 @@ export default {
onSuccess: this.taskListUpdateSuccess.bind(this),
onError: this.taskListUpdateError.bind(this),
});
+
+ this.renderSortableLists();
}
},
+ renderSortableLists() {
+ this.removeAllPointerEventListeners();
+
+ const lists = document.querySelectorAll('.description ul, .description ol');
+ lists.forEach((list) => {
+ Array.from(list.children).forEach((listItem) => {
+ listItem.prepend(this.createDragIconElement());
+ this.addPointerEventListeners(listItem);
+ });
+
+ Sortable.create(
+ list,
+ getSortableDefaultOptions({
+ handle: '.drag-icon',
+ onUpdate: (event) => {
+ const description = convertDescriptionWithNewSort(this.descriptionText, event.to);
+ this.$emit('listItemReorder', description);
+ },
+ }),
+ );
+ });
+ },
+ createDragIconElement() {
+ const container = document.createElement('div');
+ container.innerHTML = `<svg class="drag-icon s14 gl-icon gl-cursor-grab gl-visibility-hidden" role="img" aria-hidden="true">
+ <use href="${gon.sprite_icons}#drag-vertical"></use>
+ </svg>`;
+ return container.firstChild;
+ },
+ addPointerEventListeners(listItem) {
+ const pointeroverListener = (event) => {
+ if (isDragging() || this.isUpdating) {
+ return;
+ }
+ event.target.closest('li').querySelector('.drag-icon').style.visibility = 'visible'; // eslint-disable-line no-param-reassign
+ };
+ const pointeroutListener = (event) => {
+ event.target.closest('li').querySelector('.drag-icon').style.visibility = 'hidden'; // eslint-disable-line no-param-reassign
+ };
+
+ // We use pointerover/pointerout instead of CSS so that when we hover over a
+ // list item with children, the drag icons of its children do not become visible.
+ listItem.addEventListener('pointerover', pointeroverListener);
+ listItem.addEventListener('pointerout', pointeroutListener);
+ this.pointerEventListeners = this.pointerEventListeners || new Map();
+ this.pointerEventListeners.set(listItem, [
+ { type: 'pointerover', listener: pointeroverListener },
+ { type: 'pointerout', listener: pointeroutListener },
+ ]);
+ },
+ removeAllPointerEventListeners() {
+ this.pointerEventListeners?.forEach((events, listItem) => {
+ events.forEach((event) => listItem.removeEventListener(event.type, event.listener));
+ this.pointerEventListeners.delete(listItem);
+ });
+ },
taskListUpdateStarted() {
this.$emit('taskListUpdateStarted');
},
@@ -214,7 +287,10 @@ export default {
taskListFields.forEach((item, index) => {
const taskLink = item.querySelector('.gfm-issue');
if (taskLink) {
- const { issue, referenceType } = taskLink.dataset;
+ const { issue, referenceType, issueType } = taskLink.dataset;
+ if (issueType !== workItemTypes.TASK) {
+ return;
+ }
const workItemId = convertToGraphQLId(TYPE_WORK_ITEM, issue);
this.addHoverListeners(taskLink, workItemId);
taskLink.addEventListener('click', (e) => {
@@ -237,10 +313,9 @@ export default {
'btn-md',
'gl-button',
'btn-default-tertiary',
- 'gl-left-0',
'gl-p-0!',
- 'gl-top-2',
- 'gl-absolute',
+ 'gl-mt-n1',
+ 'gl-ml-3',
'js-add-task',
);
button.id = `js-task-button-${index}`;
@@ -252,7 +327,7 @@ export default {
`;
button.setAttribute('aria-label', s__('WorkItem|Convert to work item'));
button.addEventListener('click', () => this.openCreateTaskModal(button));
- item.prepend(button);
+ item.append(button);
});
},
addHoverListeners(taskLink, id) {
diff --git a/app/assets/javascripts/issues/show/utils.js b/app/assets/javascripts/issues/show/utils.js
new file mode 100644
index 00000000000..60e66f59f92
--- /dev/null
+++ b/app/assets/javascripts/issues/show/utils.js
@@ -0,0 +1,99 @@
+import { COLON, HYPHEN, NEWLINE } from '~/lib/utils/text_utility';
+
+/**
+ * Get the index from sourcepos that represents the line of
+ * the description when the description is split by newline.
+ *
+ * @param {String} sourcepos Source position in format `23:3-23:14`
+ * @returns {Number} Index of description split by newline
+ */
+const getDescriptionIndex = (sourcepos) => {
+ const [startRange] = sourcepos.split(HYPHEN);
+ const [startRow] = startRange.split(COLON);
+ return startRow - 1;
+};
+
+/**
+ * Given a `ul` or `ol` element containing a new sort order, this function performs
+ * a depth-first search to get the new sort order in the form of sourcepos indices.
+ *
+ * @param {HTMLElement} list A `ul` or `ol` element containing a new sort order
+ * @returns {Array<Number>} An array representing the new order of the list
+ */
+const getNewSourcePositions = (list) => {
+ const newSourcePositions = [];
+
+ function pushPositionOfChildListItems(el) {
+ if (!el) {
+ return;
+ }
+ if (el.tagName === 'LI') {
+ newSourcePositions.push(getDescriptionIndex(el.dataset.sourcepos));
+ }
+ Array.from(el.children).forEach(pushPositionOfChildListItems);
+ }
+
+ pushPositionOfChildListItems(list);
+
+ return newSourcePositions;
+};
+
+/**
+ * Converts a description to one with a new list sort order.
+ *
+ * Given a description like:
+ *
+ * <pre>
+ * 1. I am text
+ * 2.
+ * 3. - Item 1
+ * 4. - Item 2
+ * 5. - Item 3
+ * 6. - Item 4
+ * 7. - Item 5
+ * </pre>
+ *
+ * And a reordered list (due to dragging Item 2 into Item 1's position) like:
+ *
+ * <pre>
+ * <ul data-sourcepos="3:1-8:0">
+ * <li data-sourcepos="4:1-4:8">
+ * Item 2
+ * <ul data-sourcepos="5:1-6:10">
+ * <li data-sourcepos="5:1-5:10">Item 3</li>
+ * <li data-sourcepos="6:1-6:10">Item 4</li>
+ * </ul>
+ * </li>
+ * <li data-sourcepos="3:1-3:8">Item 1</li>
+ * <li data-sourcepos="7:1-8:0">Item 5</li>
+ * <ul>
+ * </pre>
+ *
+ * This function returns:
+ *
+ * <pre>
+ * 1. I am text
+ * 2.
+ * 3. - Item 2
+ * 4. - Item 3
+ * 5. - Item 4
+ * 6. - Item 1
+ * 7. - Item 5
+ * </pre>
+ *
+ * @param {String} description Description in markdown format
+ * @param {HTMLElement} list A `ul` or `ol` element containing a new sort order
+ * @returns {String} Markdown with a new list sort order
+ */
+export const convertDescriptionWithNewSort = (description, list) => {
+ const descriptionLines = description.split(NEWLINE);
+ const startIndexOfList = getDescriptionIndex(list.dataset.sourcepos);
+
+ getNewSourcePositions(list)
+ .map((lineIndex) => descriptionLines[lineIndex])
+ .forEach((line, index) => {
+ descriptionLines[startIndexOfList + index] = line;
+ });
+
+ return descriptionLines.join(NEWLINE);
+};
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index 85fe5ed7e26..396b015ad83 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -94,7 +94,7 @@ export default {
'emptyStateIllustration',
'isScrollingDown',
'emptyStateAction',
- 'hasRunnersForProject',
+ 'hasOfflineRunnersForProject',
]),
shouldRenderContent() {
@@ -220,7 +220,7 @@ export default {
<!-- Body Section -->
<stuck-block
v-if="job.stuck"
- :has-no-runners-for-project="hasRunnersForProject"
+ :has-offline-runners-for-project="hasOfflineRunnersForProject"
:tags="job.tags"
:runners-path="runnerSettingsUrl"
/>
diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue
index abd0c13702a..f9cde61e917 100644
--- a/app/assets/javascripts/jobs/components/stuck_block.vue
+++ b/app/assets/javascripts/jobs/components/stuck_block.vue
@@ -11,7 +11,7 @@ export default {
GlLink,
},
props: {
- hasNoRunnersForProject: {
+ hasOfflineRunnersForProject: {
type: Boolean,
required: true,
},
@@ -37,7 +37,7 @@ export default {
dataTestId: 'job-stuck-with-tags',
showTags: true,
};
- } else if (this.hasNoRunnersForProject) {
+ } else if (this.hasOfflineRunnersForProject) {
return {
text: s__(`Job|This job is stuck because the project
doesn't have any runners online assigned to it.`),
diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js
index 5849a31e9e6..a0f9db7409d 100644
--- a/app/assets/javascripts/jobs/store/getters.js
+++ b/app/assets/javascripts/jobs/store/getters.js
@@ -46,5 +46,5 @@ export const shouldRenderSharedRunnerLimitWarning = (state) =>
export const isScrollingDown = (state) => isScrolledToBottom() && !state.isJobLogComplete;
-export const hasRunnersForProject = (state) =>
+export const hasOfflineRunnersForProject = (state) =>
state?.job?.runners?.available && !state?.job?.runners?.online;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 419afa0a0a9..dad9cbcb6f6 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -6,6 +6,10 @@ import {
} from '~/lib/utils/constants';
import { allSingleQuotes } from '~/lib/utils/regexp';
+export const COLON = ':';
+export const HYPHEN = '-';
+export const NEWLINE = '\n';
+
/**
* Adds a , to a string composed by numbers, at every 3 chars.
*
diff --git a/app/assets/javascripts/sortable/constants.js b/app/assets/javascripts/sortable/constants.js
index 7fddac00ab2..f5bb0a3b11f 100644
--- a/app/assets/javascripts/sortable/constants.js
+++ b/app/assets/javascripts/sortable/constants.js
@@ -1,3 +1,5 @@
+export const DRAG_CLASS = 'is-dragging';
+
/**
* Default config options for sortablejs.
* @type {object}
@@ -12,7 +14,7 @@
export const defaultSortableOptions = {
animation: 200,
forceFallback: true,
- fallbackClass: 'is-dragging',
+ fallbackClass: DRAG_CLASS,
fallbackOnBody: true,
ghostClass: 'is-ghost',
fallbackTolerance: 1,
diff --git a/app/assets/javascripts/sortable/utils.js b/app/assets/javascripts/sortable/utils.js
index c2c8fb03b58..88ac1295a39 100644
--- a/app/assets/javascripts/sortable/utils.js
+++ b/app/assets/javascripts/sortable/utils.js
@@ -1,13 +1,17 @@
/* global DocumentTouch */
-import { defaultSortableOptions } from './constants';
+import { defaultSortableOptions, DRAG_CLASS } from './constants';
export function sortableStart() {
- document.body.classList.add('is-dragging');
+ document.body.classList.add(DRAG_CLASS);
}
export function sortableEnd() {
- document.body.classList.remove('is-dragging');
+ document.body.classList.remove(DRAG_CLASS);
+}
+
+export function isDragging() {
+ return document.body.classList.contains(DRAG_CLASS);
}
export function getSortableDefaultOptions(options) {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 1d83a61f2ae..6c99a749edc 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -42,21 +42,30 @@ export default {
<div v-if="showCommentToolBar" class="comment-toolbar clearfix">
<div class="toolbar-text">
<template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
- <gl-link :href="markdownDocsPath" target="_blank">
- {{ __('Markdown is supported') }}
- </gl-link>
+ <gl-sprintf
+ :message="
+ s__('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}')
+ "
+ >
+ <template #markdownDocsLink="{ content }">
+ <gl-link :href="markdownDocsPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
</template>
<template v-if="hasQuickActionsDocsPath && markdownDocsPath">
<gl-sprintf
:message="
- __(
- '%{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd} and %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd} are supported',
+ s__(
+ 'NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.',
)
"
>
<template #markdownDocsLink="{ content }">
<gl-link :href="markdownDocsPath" target="_blank">{{ content }}</gl-link>
</template>
+ <template #keyboard="{ content }">
+ <kbd>{{ content }}</kbd>
+ </template>
<template #quickActionsDocsLink="{ content }">
<gl-link :href="quickActionsDocsPath" target="_blank">{{ content }}</gl-link>
</template>
diff --git a/app/assets/stylesheets/errors.scss b/app/assets/stylesheets/errors.scss
index dc08c816d7d..00cc3409fa7 100644
--- a/app/assets/stylesheets/errors.scss
+++ b/app/assets/stylesheets/errors.scss
@@ -118,3 +118,11 @@ a {
}
}
}
+
+.tanuki-logo {
+ width: 210px;
+ height: 210px;
+ max-width: 40vw;
+ display: block;
+ margin: map-get($spacing-scale, 4) auto;
+}
diff --git a/app/assets/stylesheets/page_bundles/issues_show.scss b/app/assets/stylesheets/page_bundles/issues_show.scss
index 1642e6a39f3..9873a0121c0 100644
--- a/app/assets/stylesheets/page_bundles/issues_show.scss
+++ b/app/assets/stylesheets/page_bundles/issues_show.scss
@@ -1,10 +1,46 @@
@import 'mixins_and_variables_and_functions';
-.description.work-items-enabled {
+.description {
+ ul,
+ ol {
+ /* We're changing list-style-position to inside because the default of outside
+ * doesn't move the negative margin to the left of the bullet. */
+ list-style-position: inside;
+ }
+
+ li {
+ position: relative;
+ /* In the browser, the li element comes after (to the right of) the bullet point, so hovering
+ * over the left of the bullet point doesn't trigger a row hover. To trigger hovering on the
+ * left, we're applying negative margin here to shift the li element left. */
+ margin-inline-start: -1rem;
+ padding-inline-start: 2.5rem;
+
+ .drag-icon {
+ position: absolute;
+ inset-block-start: 0.3rem;
+ inset-inline-start: 1rem;
+ }
+ }
+
ul.task-list {
> li.task-list-item {
+ /* We're using !important to override the same selector in typography.scss */
+ margin-inline-start: -1rem !important;
padding-inline-start: 2.5rem;
+ > input.task-list-item-checkbox {
+ position: static;
+ vertical-align: middle;
+ margin-block-start: -2px;
+ }
+ }
+ }
+}
+
+.description.work-items-enabled {
+ ul.task-list {
+ > li.task-list-item {
.js-add-task {
svg {
visibility: hidden;
@@ -15,10 +51,6 @@
}
}
- > input.task-list-item-checkbox {
- left: 1.25rem;
- }
-
&:hover,
&:focus-within {
.js-add-task svg {
@@ -28,3 +60,8 @@
}
}
}
+
+.is-ghost {
+ opacity: 0.3;
+ pointer-events: none;
+}
diff --git a/app/controllers/pwa_controller.rb b/app/controllers/pwa_controller.rb
new file mode 100644
index 00000000000..ea14dfb27b3
--- /dev/null
+++ b/app/controllers/pwa_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class PwaController < ApplicationController # rubocop:disable Gitlab/NamespacedClass
+ layout 'errors'
+
+ feature_category :navigation
+
+ skip_before_action :authenticate_user!
+
+ def offline
+ end
+end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a2730c53ea2..e1c9e7d3896 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -246,13 +246,13 @@ module MergeRequestsHelper
''
end
- link_to branch, branch_path, title: branch, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-p-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n3'
+ link_to branch, branch_path, title: branch, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n2'
end
def merge_request_header(project, merge_request)
link_to_author = link_to_member(project, merge_request.author, size: 24, extra_class: 'gl-font-weight-bold', avatar: false)
copy_button = clipboard_button(text: merge_request.source_branch, title: _('Copy branch name'), class: 'btn btn-default btn-sm gl-button btn-default-tertiary btn-icon gl-display-none! gl-md-display-inline-block! js-source-branch-copy')
- target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), title: merge_request.target_branch, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-p-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n3'
+ target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), title: merge_request.target_branch, class: 'gl-link gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mb-n2'
_('%{author} requested to merge %{source_branch} %{copy_button} into %{target_branch} %{created_at}').html_safe % { author: link_to_author.html_safe, source_branch: merge_request_source_branch(merge_request).html_safe, copy_button: copy_button.html_safe, target_branch: target_branch.html_safe, created_at: time_ago_with_tooltip(merge_request.created_at, html_class: 'gl-display-inline-block').html_safe }
end
diff --git a/app/views/pwa/offline.html.haml b/app/views/pwa/offline.html.haml
new file mode 100644
index 00000000000..5eae546bea9
--- /dev/null
+++ b/app/views/pwa/offline.html.haml
@@ -0,0 +1,31 @@
+= link_to root_path do
+ = render 'shared/logo.svg'
+%h1= _('Offline')
+.container
+ %h3= _('You are currently offline, or the GitLab instance is not reachable.')
+ %p= _("In the background, we're attempting to connect you again.")
+ -# haml-lint:disable InlineJavaScript
+ :javascript
+ window.addEventListener('online', () => {
+ window.location.reload();
+ });
+
+ async function checkNetworkAndReload() {
+ try {
+ const response = await fetch('.');
+ // Verify we get a valid response from the server
+ if (response.status >= 200 && response.status < 500) {
+ window.location.reload();
+ return;
+ }
+ } catch {
+ // Unable to connect to the server, ignore.
+ }
+ window.setTimeout(checkNetworkAndReload, 2500);
+ }
+
+ if (window.location.pathname.endsWith('/-/offline')) {
+ return;
+ }
+
+ checkNetworkAndReload();
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 8a79a17b166..c845d4df7df 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -2,15 +2,12 @@
- supports_file_upload = local_assigns.fetch(:supports_file_upload, true)
.comment-toolbar.clearfix
.toolbar-text
- = link_to _('Markdown'), help_page_path('user/markdown'), target: '_blank', rel: 'noopener noreferrer'
+ - markdownLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/markdown') }
+ - quickActionsLinkStart = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/quick_actions') }
- if supports_quick_actions
- and
- = link_to _('quick actions'), help_page_path('user/project/quick_actions'), target: '_blank', rel: 'noopener noreferrer'
- are
+ = html_escape(s_('NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}.')) % { markdownDocsLinkStart: markdownLinkStart, markdownDocsLinkEnd: '</a>'.html_safe, quickActionsDocsLinkStart: quickActionsLinkStart, quickActionsDocsLinkEnd: '</a>'.html_safe, keyboardStart: '<kbd>'.html_safe, keyboardEnd: '</kbd>'.html_safe }
- else
- is
- supported
-
+ = html_escape(s_('MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}')) % { markdownDocsLinkStart: markdownLinkStart, markdownDocsLinkEnd: '</a>'.html_safe }
- if supports_file_upload
%span.uploading-container
%span.uploading-progress-container.hide
diff --git a/config/feature_flags/development/automated_email_provision.yml b/config/feature_flags/development/automated_email_provision.yml
index 2b3fee208ab..2734413dd8f 100644
--- a/config/feature_flags/development/automated_email_provision.yml
+++ b/config/feature_flags/development/automated_email_provision.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348317
milestone: '14.6'
type: development
group: group::license
-default_enabled: false
+default_enabled: true
diff --git a/config/routes.rb b/config/routes.rb
index 4154c0270c6..b2b4bece68a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -116,6 +116,8 @@ Rails.application.routes.draw do
get '/whats_new' => 'whats_new#index'
+ get 'offline' => "pwa#offline"
+
# '/-/health' implemented by BasicHealthCheck middleware
get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness'
diff --git a/data/removals/15_0/15-0-secret-detection-configurations.yml b/data/removals/15_0/15-0-secret-detection-configurations.yml
new file mode 100644
index 00000000000..0f759fae11c
--- /dev/null
+++ b/data/removals/15_0/15-0-secret-detection-configurations.yml
@@ -0,0 +1,30 @@
+- name: "Secret Detection configuration variables"
+ announcement_milestone: "14.8"
+ announcement_date: "2022-02-22"
+ removal_milestone: "15.0"
+ removal_date: "2022-05-22"
+ breaking_change: false
+ reporter: connorgilbert
+ stage: Secure
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352565
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ To make it simpler and more reliable to [customize GitLab Secret Detection](https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings), we've removed some of the variables that you could previously set in your CI/CD configuration.
+
+ The following variables previously allowed you to customize the options for historical scanning, but interacted poorly with the [GitLab-managed CI/CD template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml) and are now removed:
+
+ - `SECRET_DETECTION_COMMIT_FROM`
+ - `SECRET_DETECTION_COMMIT_TO`
+ - `SECRET_DETECTION_COMMITS`
+ - `SECRET_DETECTION_COMMITS_FILE`
+
+ The `SECRET_DETECTION_ENTROPY_LEVEL` previously allowed you to configure rules that only considered the entropy level of strings in your codebase, and is now removed.
+ This type of entropy-only rule created an unacceptable number of incorrect results (false positives).
+
+ You can still customize the behavior of the Secret Detection analyzer using the [available CI/CD variables](https://docs.gitlab.com/ee/user/application_security/secret_detection/#available-cicd-variables).
+
+ For further details, see [the deprecation issue for this change](https://gitlab.com/gitlab-org/gitlab/-/issues/352565).
+# The following items are not published on the docs page, but may be used in the future.
+ tiers: [Free, Silver, Gold, Core, Premium, Ultimate]
+ documentation_url: https://docs.gitlab.com/ee/user/application_security/secret_detection/#available-cicd-variables # (optional) This is a link to the current documentation page
+ image_url: # (optional) This is a link to a thumbnail image depicting the feature
+ video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
diff --git a/doc/development/snowplow/troubleshooting.md b/doc/development/snowplow/troubleshooting.md
index 597e0436c67..2a6db80a6f2 100644
--- a/doc/development/snowplow/troubleshooting.md
+++ b/doc/development/snowplow/troubleshooting.md
@@ -48,3 +48,27 @@ Already conducted investigations:
### Troubleshooting data warehouse layer
Reach out to [Data team](https://about.gitlab.com/handbook/business-technology/data-team/) to ask about current state of data warehouse. On their handbook page there is a [section with contact details](https://about.gitlab.com/handbook/business-technology/data-team/#how-to-connect-with-us)
+
+## Delay in Snowplow Enrichers
+
+If there is an alert for **Snowplow Raw Good Stream Backing Up**, we receive an email notification. This sometimes happens because Snowplow Enrichers don't scale well enough for the amount of Snowplow events.
+
+If the delay goes over 48 hours, we lose data.
+
+### Contact SRE on-call
+
+Send a message in the [#infrastructure_lounge](https://gitlab.slack.com/archives/CB3LSMEJV) Slack channel using the following template:
+
+```markdown
+Hello team!
+
+We received an alert for [Snowplow Raw Good Stream Backing Up](https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#alarmsV2:alarm/SnowPlow+Raw+Good+Stream+Backing+Up?).
+
+Enrichers are not scalling well for the amount of events we receive.
+
+See the [dashboard](https://us-east-1.console.aws.amazon.com/cloudwatch/home?region=us-east-1#dashboards:name=SnowPlow).
+
+Could we get assistance in order to fix the delay?
+
+Thank you!
+```
diff --git a/doc/update/removals.md b/doc/update/removals.md
index cbb86f721a6..79e03f5b5eb 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -512,6 +512,24 @@ changes to your code, settings, or workflow.
Long term service and support (LTSS) for SUSE Linux Enterprise Server (SLES) 12 SP2 [ended on March 31, 2021](https://www.suse.com/lifecycle/). The CA certificates on SP2 include the expired DST root certificate, and it's not getting new CA certificate package updates. We have implemented some [workarounds](https://gitlab.com/gitlab-org/gitlab-omnibus-builder/-/merge_requests/191), but we will not be able to continue to keep the build running properly.
+### Secret Detection configuration variables
+
+To make it simpler and more reliable to [customize GitLab Secret Detection](https://docs.gitlab.com/ee/user/application_security/secret_detection/#customizing-settings), we've removed some of the variables that you could previously set in your CI/CD configuration.
+
+The following variables previously allowed you to customize the options for historical scanning, but interacted poorly with the [GitLab-managed CI/CD template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/Secret-Detection.gitlab-ci.yml) and are now removed:
+
+- `SECRET_DETECTION_COMMIT_FROM`
+- `SECRET_DETECTION_COMMIT_TO`
+- `SECRET_DETECTION_COMMITS`
+- `SECRET_DETECTION_COMMITS_FILE`
+
+The `SECRET_DETECTION_ENTROPY_LEVEL` previously allowed you to configure rules that only considered the entropy level of strings in your codebase, and is now removed.
+This type of entropy-only rule created an unacceptable number of incorrect results (false positives).
+
+You can still customize the behavior of the Secret Detection analyzer using the [available CI/CD variables](https://docs.gitlab.com/ee/user/application_security/secret_detection/#available-cicd-variables).
+
+For further details, see [the deprecation issue for this change](https://gitlab.com/gitlab-org/gitlab/-/issues/352565).
+
### Sidekiq configuration for metrics and health checks
WARNING:
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index f50042975b3..d0487dc57ba 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -184,8 +184,8 @@ include:
The `CS_DISABLE_DEPENDENCY_LIST` CI/CD variable controls whether the scan creates a
[Dependency List](../dependency_list/)
-report. The variable's default setting of `false` causes the scan to create the report. To disable
-the report, set the variable to `true`:
+report. This variable is currently only supported when the `trivy` analyzer is used. The variable's default setting of `"false"` causes the scan to create the report. To disable
+the report, set the variable to `"true"`:
For example:
diff --git a/doc/user/application_security/secret_detection/index.md b/doc/user/application_security/secret_detection/index.md
index 0a18e7d5f45..3937cbd77b6 100644
--- a/doc/user/application_security/secret_detection/index.md
+++ b/doc/user/application_security/secret_detection/index.md
@@ -195,13 +195,18 @@ Secret Detection can be customized by defining available CI/CD variables:
| CI/CD variable | Default value | Description |
|-----------------------------------|---------------|-------------|
-| `SECRET_DETECTION_COMMIT_FROM` | - | The commit a Gitleaks scan starts at. [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243564) in GitLab 13.5. Replaced with `SECRET_DETECTION_COMMITS`. |
-| `SECRET_DETECTION_COMMIT_TO` | - | The commit a Gitleaks scan ends at. [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243564) in GitLab 13.5. Replaced with `SECRET_DETECTION_COMMITS`. |
-| `SECRET_DETECTION_COMMITS` | - | The list of commits that Gitleaks should scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/243564) in GitLab 13.5. |
| `SECRET_DETECTION_EXCLUDED_PATHS` | "" | Exclude vulnerabilities from output based on the paths. This is a comma-separated list of patterns. Patterns can be globs, or file or folder paths (for example, `doc,spec` ). Parent directories also match patterns. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/225273) in GitLab 13.3. |
| `SECRET_DETECTION_HISTORIC_SCAN` | false | Flag to enable a historic Gitleaks scan. |
| `SECRET_DETECTION_IMAGE_SUFFIX` | Suffix added to the image name. If set to `-fips`, `FIPS-enabled` images are used for scan. See [FIPS-enabled images](#fips-enabled-images) for more details. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/355519) in GitLab 14.10. |
+In previous GitLab versions, the following variables were also available:
+
+| CI/CD variable | Default value | Description |
+|-----------------------------------|---------------|-------------|
+| `SECRET_DETECTION_COMMIT_FROM` | - | The commit a Gitleaks scan starts at. [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243564) in GitLab 13.5. Replaced with `SECRET_DETECTION_COMMITS`. |
+| `SECRET_DETECTION_COMMIT_TO` | - | The commit a Gitleaks scan ends at. [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243564) in GitLab 13.5. Replaced with `SECRET_DETECTION_COMMITS`. |
+| `SECRET_DETECTION_COMMITS` | - | The list of commits that Gitleaks should scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/243564) in GitLab 13.5. [Removed](https://gitlab.com/gitlab-org/gitlab/-/issues/352565) in GitLab 15.0. |
+
### Custom rulesets **(ULTIMATE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/211387) in GitLab 13.5.
diff --git a/doc/user/free_user_limit.md b/doc/user/free_user_limit.md
new file mode 100644
index 00000000000..2340ef254f6
--- /dev/null
+++ b/doc/user/free_user_limit.md
@@ -0,0 +1,60 @@
+---
+stage: Growth
+group: Conversion
+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
+---
+
+# Free user limit **(FREE SAAS)**
+
+In GitLab 15.1 (June 22, 2022) and later, namespaces in GitLab.com on the Free tier
+will be limited to five (5) members per [namespace](group/index.md#namespaces).
+This limit applies to top-level groups and personal namespaces.
+
+In a personal namespace, the limit applies across all projects in your personal
+namespace.
+
+On the transition date, if your namespace has six or more unique members:
+
+- Five members will keep a status of `Active`.
+- Remaining members will get a status of `Over limit` and lose access to the
+ group.
+- Members invited through a group or project invitation outside of the namespace
+ will be removed. You can add these members back by inviting them through their
+ username or email address on the **Members** page for your group or project.
+
+## How active members are determined
+
+On the transition date, we'll automatically select the members who keep their `Active` status
+in the following order, until we reach a total of five:
+
+1. Members with the Owner or Maintainer role.
+1. The most recently active members.
+
+## Manage members in your namespace
+
+To help manage your free user limit,
+you can view and manage the total number of members across all projects and groups
+in your namespace.
+
+Prerequisite:
+
+- You must have the Owner role for the group.
+
+1. On the top bar, select **Menu > Groups** and find your group.
+1. On the left sidebar, select **Settings > Usage Quotas**.
+1. To view all members, select the **Seats** tab.
+1. To remove a member, select **Remove user**.
+
+NOTE:
+The **Usage Quotas** page is not available for personal namespaces. You can
+view and [remove members](project/members/index.md#remove-a-member-from-a-project)
+in each project instead. The five user limit includes all
+unique members across all projects in your personal namespace.
+
+If you need more time to manage your members, or to try GitLab features
+with a team of more than five members, you can [start a trial](https://about.gitlab.com/free-trial/).
+A trial lasts for 30 days and includes an unlimited number of members.
+
+## Related topics
+
+- [GitLab SaaS Free tier frequently asked questions](https://about.gitlab.com/pricing/faq-efficient-free-tier/)
diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md
index 4508ef30ac5..7db66dd013b 100644
--- a/doc/user/project/issues/managing_issues.md
+++ b/doc/user/project/issues/managing_issues.md
@@ -225,6 +225,8 @@ When you're creating a new issue, you can complete the following fields:
## Edit an issue
+> Reordering list items [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15260) in GitLab 15.0.
+
You can edit an issue's title and description.
Prerequisites:
@@ -237,6 +239,14 @@ To edit an issue:
1. Edit the available fields.
1. Select **Save changes**.
+You can also reorder list items, which include bullet, numerical, and task list items.
+To reorder list items:
+
+1. Hover over the list item row to make the drag icon visible.
+1. Click and hold the drag icon.
+1. Drag the row to the new position in the list.
+1. Release the drag icon.
+
### Bulk edit issues from a project
> - Assigning epic [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210470) in GitLab 13.2.
diff --git a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
index d299fa871b7..4460843545e 100644
--- a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
+++ b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb
@@ -154,7 +154,6 @@ module Gitlab
def handle_unsupported_report_version(treat_as:)
if report_version.nil?
message = "Report version not provided, #{report_type} report type supports versions: #{supported_schema_versions}"
- add_message_as(level: treat_as, message: message)
else
message = "Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: #{supported_schema_versions}"
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a260318cd6e..81255bed4c6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -772,9 +772,6 @@ msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
-msgid "%{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd} and %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd} are supported"
-msgstr ""
-
msgid "%{mergeLength}/%{usersLength} can merge"
msgstr ""
@@ -19357,6 +19354,9 @@ msgstr ""
msgid "In progress"
msgstr ""
+msgid "In the background, we're attempting to connect you again."
+msgstr ""
+
msgid "In this page you will find information about the settings that are used in your current instance."
msgstr ""
@@ -23282,18 +23282,12 @@ msgstr ""
msgid "Mark to do as done"
msgstr ""
-msgid "Markdown"
-msgstr ""
-
msgid "Markdown Help"
msgstr ""
msgid "Markdown enabled."
msgstr ""
-msgid "Markdown is supported"
-msgstr ""
-
msgid "Markdown supported."
msgstr ""
@@ -23321,6 +23315,9 @@ msgstr ""
msgid "MarkdownEditor|Add strikethrough text (%{modifier_key}⇧X)"
msgstr ""
+msgid "MarkdownToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}"
+msgstr ""
+
msgid "Marked For Deletion At - %{deletion_time}"
msgstr ""
@@ -25789,6 +25786,9 @@ msgstr ""
msgid "NoteForm|Note"
msgstr ""
+msgid "NoteToolbar|Supports %{markdownDocsLinkStart}Markdown%{markdownDocsLinkEnd}. For %{quickActionsDocsLinkStart}quick actions%{quickActionsDocsLinkEnd}, type %{keyboardStart}/%{keyboardEnd}."
+msgstr ""
+
msgid "Notes"
msgstr ""
@@ -26058,6 +26058,9 @@ msgstr ""
msgid "Off"
msgstr ""
+msgid "Offline"
+msgstr ""
+
msgid "Oh no!"
msgstr ""
@@ -42992,6 +42995,9 @@ msgstr ""
msgid "You are connected to the Prometheus server, but there is currently no data to display."
msgstr ""
+msgid "You are currently offline, or the GitLab instance is not reachable."
+msgstr ""
+
msgid "You are going to delete %{project_full_name}. Deleted projects CANNOT be restored! Are you ABSOLUTELY sure?"
msgstr ""
@@ -45566,9 +45572,6 @@ msgstr ""
msgid "projects"
msgstr ""
-msgid "quick actions"
-msgstr ""
-
msgid "reCAPTCHA"
msgstr ""
diff --git a/qa/qa/page/component/blob_content.rb b/qa/qa/page/component/blob_content.rb
index ce743b24dda..c2a1687ccfc 100644
--- a/qa/qa/page/component/blob_content.rb
+++ b/qa/qa/page/component/blob_content.rb
@@ -63,6 +63,17 @@ module QA
end
end
+ def has_normalized_ws_text?(text, wait: Capybara.default_max_wait_time)
+ if has_element?(:blob_viewer_file_content, wait: 1)
+ # The blob viewer renders line numbers and whitespace in a way that doesn't match the source file
+ # This isn't a visual validation test, so we ignore line numbers and whitespace
+ find_element(:blob_viewer_file_content, wait: 0).text.gsub(/^\d+\s|\s*/, '')
+ .start_with?(text.gsub(/\s*/, ''))
+ else
+ has_text?(text.gsub(/\s+/, " "), wait: wait)
+ end
+ end
+
def click_copy_file_contents(file_number = nil)
within_file_by_number(:default_actions_container, file_number) { click_element(:copy_contents_button) }
end
diff --git a/qa/qa/page/file/form.rb b/qa/qa/page/file/form.rb
index a6251f185f9..bb8934db498 100644
--- a/qa/qa/page/file/form.rb
+++ b/qa/qa/page/file/form.rb
@@ -4,8 +4,9 @@ module QA
module Page
module File
class Form < Page::Base
- include Shared::CommitMessage
include Page::Component::DropdownFilter
+ include Page::Component::BlobContent
+ include Shared::CommitMessage
include Shared::CommitButton
include Shared::Editor
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 30d1895781a..f3ee627c41e 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -133,6 +133,14 @@ module QA
has_css?('[name="arkose_labs_token"][value]', visible: false)
end
+ def has_accept_all_cookies_button?
+ has_button?('Accept All Cookies')
+ end
+
+ def click_accept_all_cookies
+ click_button('Accept All Cookies')
+ end
+
def switch_to_sign_in_tab
click_element :sign_in_tab
end
@@ -179,6 +187,7 @@ module QA
fill_element :password_field, user.password
if Runtime::Env.running_on_dot_com?
+ click_accept_all_cookies if has_accept_all_cookies_button?
# Arkose only appears in staging.gitlab.com, gitlab.com, etc...
# Wait until the ArkoseLabs challenge has initialized
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index c4c1ee6347e..4b800a4ccc9 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import '~/behaviors/markdown/render_gfm';
import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
import IssuableApp from '~/issues/show/components/app.vue';
@@ -647,4 +648,14 @@ describe('Issuable output', () => {
expect(wrapper.vm.updateStoreState).toHaveBeenCalled();
});
});
+
+ describe('listItemReorder event', () => {
+ it('makes request to update issue', async () => {
+ const description = 'I have been updated!';
+ findDescription().vm.$emit('listItemReorder', description);
+ await waitForPromises();
+
+ expect(mock.history.put[0].data).toContain(description);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 7b0b8ca686a..909789b7a0f 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -77,7 +77,22 @@ export const descriptionHtmlWithTask = `
<ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
<li data-sourcepos="1:1-1:10" class="task-list-item">
<input type="checkbox" class="task-list-item-checkbox" disabled>
- <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip">1 (#48)</a>
+ <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="task">1 (#48)</a>
+ </li>
+ <li data-sourcepos="2:1-2:7" class="task-list-item">
+ <input type="checkbox" class="task-list-item-checkbox" disabled> 2
+ </li>
+ <li data-sourcepos="3:1-3:7" class="task-list-item">
+ <input type="checkbox" class="task-list-item-checkbox" disabled> 3
+ </li>
+ </ul>
+`;
+
+export const descriptionHtmlWithIssue = `
+ <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
+ <li data-sourcepos="1:1-1:10" class="task-list-item">
+ <input type="checkbox" class="task-list-item-checkbox" disabled>
+ <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="issue">1 (#48)</a>
</li>
<li data-sourcepos="2:1-2:7" class="task-list-item">
<input type="checkbox" class="task-list-item-checkbox" disabled> 2
diff --git a/spec/frontend/issues/show/utils_spec.js b/spec/frontend/issues/show/utils_spec.js
new file mode 100644
index 00000000000..e5f14cfc01a
--- /dev/null
+++ b/spec/frontend/issues/show/utils_spec.js
@@ -0,0 +1,40 @@
+import { convertDescriptionWithNewSort } from '~/issues/show/utils';
+
+describe('app/assets/javascripts/issues/show/utils.js', () => {
+ describe('convertDescriptionWithNewSort', () => {
+ it('converts markdown description with new list sort order', () => {
+ const description = `I am text
+
+- Item 1
+- Item 2
+ - Item 3
+ - Item 4
+- Item 5`;
+
+ // Drag Item 2 + children to Item 1's position
+ const html = `<ul data-sourcepos="3:1-8:0">
+ <li data-sourcepos="4:1-4:8">
+ Item 2
+ <ul data-sourcepos="5:1-6:10">
+ <li data-sourcepos="5:1-5:10">Item 3</li>
+ <li data-sourcepos="6:1-6:10">Item 4</li>
+ </ul>
+ </li>
+ <li data-sourcepos="3:1-3:8">Item 1</li>
+ <li data-sourcepos="7:1-8:0">Item 5</li>
+ <ul>`;
+ const list = document.createElement('div');
+ list.innerHTML = html;
+
+ const expected = `I am text
+
+- Item 2
+ - Item 3
+ - Item 4
+- Item 1
+- Item 5`;
+
+ expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/stuck_block_spec.js b/spec/frontend/jobs/components/stuck_block_spec.js
index 4db73eaaaec..1580ed45e46 100644
--- a/spec/frontend/jobs/components/stuck_block_spec.js
+++ b/spec/frontend/jobs/components/stuck_block_spec.js
@@ -32,7 +32,7 @@ describe('Stuck Block Job component', () => {
describe('with no runners for project', () => {
beforeEach(() => {
createWrapper({
- hasNoRunnersForProject: true,
+ hasOfflineRunnersForProject: true,
runnersPath: '/root/project/runners#js-runners-settings',
});
});
@@ -53,7 +53,7 @@ describe('Stuck Block Job component', () => {
describe('with tags', () => {
beforeEach(() => {
createWrapper({
- hasNoRunnersForProject: false,
+ hasOfflineRunnersForProject: false,
tags,
runnersPath: '/root/project/runners#js-runners-settings',
});
@@ -81,7 +81,7 @@ describe('Stuck Block Job component', () => {
describe('without active runners', () => {
beforeEach(() => {
createWrapper({
- hasNoRunnersForProject: false,
+ hasOfflineRunnersForProject: false,
runnersPath: '/root/project/runners#js-runners-settings',
});
});
diff --git a/spec/frontend/jobs/store/getters_spec.js b/spec/frontend/jobs/store/getters_spec.js
index 7a0fe2b8202..c13b051c672 100644
--- a/spec/frontend/jobs/store/getters_spec.js
+++ b/spec/frontend/jobs/store/getters_spec.js
@@ -208,7 +208,7 @@ describe('Job Store Getters', () => {
});
});
- describe('hasRunnersForProject', () => {
+ describe('hasOfflineRunnersForProject', () => {
describe('with available and offline runners', () => {
it('returns true', () => {
localState.job.runners = {
@@ -216,7 +216,7 @@ describe('Job Store Getters', () => {
online: false,
};
- expect(getters.hasRunnersForProject(localState)).toEqual(true);
+ expect(getters.hasOfflineRunnersForProject(localState)).toEqual(true);
});
});
@@ -227,7 +227,7 @@ describe('Job Store Getters', () => {
online: false,
};
- expect(getters.hasRunnersForProject(localState)).toEqual(false);
+ expect(getters.hasOfflineRunnersForProject(localState)).toEqual(false);
});
});
@@ -238,7 +238,7 @@ describe('Job Store Getters', () => {
online: true,
};
- expect(getters.hasRunnersForProject(localState)).toEqual(false);
+ expect(getters.hasOfflineRunnersForProject(localState)).toEqual(false);
});
});
});
diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
index fae09c32c20..d06077d69b6 100644
--- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
+++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let_it_be(:project) { create(:project) }
+ let(:supported_dast_versions) { described_class::SUPPORTED_VERSIONS[:dast].join(', ') }
+
let(:scanner) do
{
'id' => 'gemnasium',
@@ -22,7 +24,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
expect(described_class::SUPPORTED_VERSIONS.keys).to eq(described_class::DEPRECATED_VERSIONS.keys)
end
- context 'files under schema path are explicitly listed' do
+ context 'when a schema JSON file exists for a particular report type version' do
# We only care about the part that comes before report-format.json
# https://rubular.com/r/N8Juz7r8hYDYgD
filename_regex = /(?<report_type>[-\w]*)\-report-format.json/
@@ -36,14 +38,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
matches = filename_regex.match(file)
report_type = matches[:report_type].tr("-", "_").to_sym
- it "#{report_type} #{version}" do
+ it "#{report_type} #{version} is in the constant" do
expect(described_class::SUPPORTED_VERSIONS[report_type]).to include(version)
end
end
end
end
- context 'every SUPPORTED_VERSION has a corresponding JSON file' do
+ context 'when every SUPPORTED_VERSION has a corresponding JSON file' do
described_class::SUPPORTED_VERSIONS.each_key do |report_type|
# api_fuzzing is covered by DAST schema
next if report_type == :api_fuzzing
@@ -66,7 +68,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -77,7 +79,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
it { is_expected.to be_truthy }
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
@@ -116,7 +118,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash)
end
- context 'and the report passes schema validation' do
+ context 'when the report passes schema validation' do
let(:report_data) do
{
'version' => '10.0.0',
@@ -141,8 +143,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
end
end
- context 'and the report does not pass schema validation' do
- context 'and enforce_security_report_validation is enabled' do
+ context 'when the report does not pass schema validation' do
+ context 'when enforce_security_report_validation is enabled' do
before do
stub_feature_flags(enforce_security_report_validation: true)
end
@@ -156,7 +158,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
it { is_expected.to be_falsey }
end
- context 'and enforce_security_report_validation is disabled' do
+ context 'when enforce_security_report_validation is disabled' do
before do
stub_feature_flags(enforce_security_report_validation: false)
end
@@ -176,12 +178,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { "12.37.0" }
- context 'if enforce_security_report_validation is enabled' do
+ context 'when enforce_security_report_validation is enabled' do
before do
stub_feature_flags(enforce_security_report_validation: true)
end
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -206,14 +208,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
end
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
}
end
- context 'and scanner information is empty' do
+ context 'when scanner information is empty' do
let(:scanner) { {} }
it 'logs related information' do
@@ -245,12 +247,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
end
end
- context 'if enforce_security_report_validation is disabled' do
+ context 'when enforce_security_report_validation is disabled' do
before do
stub_feature_flags(enforce_security_report_validation: false)
end
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -261,7 +263,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
it { is_expected.to be_truthy }
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
@@ -272,6 +274,30 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
end
end
end
+
+ context 'when not given a schema version' do
+ let(:report_type) { :dast }
+ let(:report_version) { nil }
+ let(:report_data) do
+ {
+ 'vulnerabilities' => []
+ }
+ end
+
+ before do
+ stub_feature_flags(enforce_security_report_validation: true)
+ end
+
+ it { is_expected.to be_falsey }
+
+ context 'when enforce_security_report_validation is disabled' do
+ before do
+ stub_feature_flags(enforce_security_report_validation: false)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
describe '#errors' do
@@ -281,7 +307,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -289,19 +315,17 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- let(:expected_errors) { [] }
-
- it { is_expected.to match_array(expected_errors) }
+ it { is_expected.to be_empty }
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
}
end
- context 'if enforce_security_report_validation is enabled' do
+ context 'when enforce_security_report_validation is enabled' do
before do
stub_feature_flags(enforce_security_report_validation: project)
end
@@ -315,14 +339,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
it { is_expected.to match_array(expected_errors) }
end
- context 'if enforce_security_report_validation is disabled' do
+ context 'when enforce_security_report_validation is disabled' do
before do
stub_feature_flags(enforce_security_report_validation: false)
end
- let(:expected_errors) { [] }
-
- it { is_expected.to match_array(expected_errors) }
+ it { is_expected.to be_empty }
end
end
end
@@ -341,7 +363,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash)
end
- context 'and the report passes schema validation' do
+ context 'when the report passes schema validation' do
let(:report_data) do
{
'version' => '10.0.0',
@@ -349,13 +371,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- let(:expected_errors) { [] }
-
- it { is_expected.to match_array(expected_errors) }
+ it { is_expected.to be_empty }
end
- context 'and the report does not pass schema validation' do
- context 'and enforce_security_report_validation is enabled' do
+ context 'when the report does not pass schema validation' do
+ context 'when enforce_security_report_validation is enabled' do
before do
stub_feature_flags(enforce_security_report_validation: true)
end
@@ -376,7 +396,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
it { is_expected.to match_array(expected_errors) }
end
- context 'and enforce_security_report_validation is disabled' do
+ context 'when enforce_security_report_validation is disabled' do
before do
stub_feature_flags(enforce_security_report_validation: false)
end
@@ -387,9 +407,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- let(:expected_errors) { [] }
-
- it { is_expected.to match_array(expected_errors) }
+ it { is_expected.to be_empty }
end
end
end
@@ -398,12 +416,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { "12.37.0" }
- context 'if enforce_security_report_validation is enabled' do
+ context 'when enforce_security_report_validation is enabled' do
before do
stub_feature_flags(enforce_security_report_validation: true)
end
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -413,14 +431,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_errors) do
[
- "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1, 14.1.2"
+ "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}"
]
end
it { is_expected.to match_array(expected_errors) }
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
@@ -429,7 +447,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_errors) do
[
- "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1, 14.1.2",
+ "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}",
"root is missing required keys: vulnerabilities"
]
end
@@ -438,12 +456,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
end
end
- context 'if enforce_security_report_validation is disabled' do
+ context 'when enforce_security_report_validation is disabled' do
before do
stub_feature_flags(enforce_security_report_validation: false)
end
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -451,22 +469,45 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- let(:expected_errors) { [] }
-
- it { is_expected.to match_array(expected_errors) }
+ it { is_expected.to be_empty }
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
}
end
- let(:expected_errors) { [] }
+ it { is_expected.to be_empty }
+ end
+ end
+ end
+
+ context 'when not given a schema version' do
+ let(:report_type) { :dast }
+ let(:report_version) { nil }
+ let(:report_data) do
+ {
+ 'vulnerabilities' => []
+ }
+ end
- it { is_expected.to match_array(expected_errors) }
+ let(:expected_errors) do
+ [
+ "root is missing required keys: version",
+ "Report version not provided, dast report type supports versions: #{supported_dast_versions}"
+ ]
+ end
+
+ it { is_expected.to match_array(expected_errors) }
+
+ context 'when enforce_security_report_validation is disabled' do
+ before do
+ stub_feature_flags(enforce_security_report_validation: false)
end
+
+ it { is_expected.to be_empty }
end
end
end
@@ -478,9 +519,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
- let(:expected_deprecation_warnings) { [] }
-
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -488,17 +527,17 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- it { is_expected.to match_array(expected_deprecation_warnings) }
+ it { is_expected.to be_empty }
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
}
end
- it { is_expected.to match_array(expected_deprecation_warnings) }
+ it { is_expected.to be_empty }
end
end
@@ -513,7 +552,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last }
let(:expected_deprecation_warnings) do
[
- "Version V2.7.0 for report type dast has been deprecated, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1, 14.1.2"
+ "Version V2.7.0 for report type dast has been deprecated, supported versions for this report type are: #{supported_dast_versions}"
]
end
@@ -521,7 +560,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash)
end
- context 'and the report passes schema validation' do
+ context 'when the report passes schema validation' do
let(:report_data) do
{
'version' => report_version,
@@ -532,7 +571,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
it { is_expected.to match_array(expected_deprecation_warnings) }
end
- context 'and the report does not pass schema validation' do
+ context 'when the report does not pass schema validation' do
let(:report_data) do
{
'version' => 'V2.7.0'
@@ -565,7 +604,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -573,29 +612,25 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- let(:expected_warnings) { [] }
-
- it { is_expected.to match_array(expected_warnings) }
+ it { is_expected.to be_empty }
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
}
end
- context 'if enforce_security_report_validation is enabled' do
+ context 'when enforce_security_report_validation is enabled' do
before do
stub_feature_flags(enforce_security_report_validation: project)
end
- let(:expected_warnings) { [] }
-
- it { is_expected.to match_array(expected_warnings) }
+ it { is_expected.to be_empty }
end
- context 'if enforce_security_report_validation is disabled' do
+ context 'when enforce_security_report_validation is disabled' do
before do
stub_feature_flags(enforce_security_report_validation: false)
end
@@ -625,36 +660,32 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash)
end
- context 'and the report passes schema validation' do
+ context 'when the report passes schema validation' do
let(:report_data) do
{
'vulnerabilities' => []
}
end
- let(:expected_warnings) { [] }
-
- it { is_expected.to match_array(expected_warnings) }
+ it { is_expected.to be_empty }
end
- context 'and the report does not pass schema validation' do
+ context 'when the report does not pass schema validation' do
let(:report_data) do
{
'version' => 'V2.7.0'
}
end
- context 'and enforce_security_report_validation is enabled' do
+ context 'when enforce_security_report_validation is enabled' do
before do
stub_feature_flags(enforce_security_report_validation: true)
end
- let(:expected_warnings) { [] }
-
- it { is_expected.to match_array(expected_warnings) }
+ it { is_expected.to be_empty }
end
- context 'and enforce_security_report_validation is disabled' do
+ context 'when enforce_security_report_validation is disabled' do
before do
stub_feature_flags(enforce_security_report_validation: false)
end
@@ -675,12 +706,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:report_type) { :dast }
let(:report_version) { "12.37.0" }
- context 'if enforce_security_report_validation is enabled' do
+ context 'when enforce_security_report_validation is enabled' do
before do
stub_feature_flags(enforce_security_report_validation: true)
end
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -688,30 +719,26 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
}
end
- let(:expected_warnings) { [] }
-
- it { is_expected.to match_array(expected_warnings) }
+ it { is_expected.to be_empty }
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
}
end
- let(:expected_warnings) { [] }
-
- it { is_expected.to match_array(expected_warnings) }
+ it { is_expected.to be_empty }
end
end
- context 'if enforce_security_report_validation is disabled' do
+ context 'when enforce_security_report_validation is disabled' do
before do
stub_feature_flags(enforce_security_report_validation: false)
end
- context 'and the report is valid' do
+ context 'when the report is valid' do
let(:report_data) do
{
'version' => report_version,
@@ -721,14 +748,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_warnings) do
[
- "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1, 14.1.2"
+ "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}"
]
end
it { is_expected.to match_array(expected_warnings) }
end
- context 'and the report is invalid' do
+ context 'when the report is invalid' do
let(:report_data) do
{
'version' => report_version
@@ -737,7 +764,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_warnings) do
[
- "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: 14.0.0, 14.0.1, 14.0.2, 14.0.3, 14.0.4, 14.0.5, 14.0.6, 14.1.0, 14.1.1, 14.1.2",
+ "Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}",
"root is missing required keys: vulnerabilities"
]
end
@@ -746,5 +773,32 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
end
end
end
+
+ context 'when not given a schema version' do
+ let(:report_type) { :dast }
+ let(:report_version) { nil }
+ let(:report_data) do
+ {
+ 'vulnerabilities' => []
+ }
+ end
+
+ it { is_expected.to be_empty }
+
+ context 'when enforce_security_report_validation is disabled' do
+ before do
+ stub_feature_flags(enforce_security_report_validation: false)
+ end
+
+ let(:expected_warnings) do
+ [
+ "root is missing required keys: version",
+ "Report version not provided, dast report type supports versions: #{supported_dast_versions}"
+ ]
+ end
+
+ it { is_expected.to match_array(expected_warnings) }
+ end
+ end
end
end
diff --git a/spec/requests/pwa_controller_spec.rb b/spec/requests/pwa_controller_spec.rb
new file mode 100644
index 00000000000..f74f37ea9d0
--- /dev/null
+++ b/spec/requests/pwa_controller_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PwaController do
+ describe 'GET #offline' do
+ it 'responds with static HTML page' do
+ get offline_path
+
+ expect(response.body).to include('You are currently offline')
+ expect(response).to have_gitlab_http_status(:success)
+ end
+ end
+end
diff --git a/spec/views/shared/notes/_form.html.haml_spec.rb b/spec/views/shared/notes/_form.html.haml_spec.rb
index b7bad4c5d78..ccf1e08b7e7 100644
--- a/spec/views/shared/notes/_form.html.haml_spec.rb
+++ b/spec/views/shared/notes/_form.html.haml_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe 'shared/notes/_form' do
let(:note) { build(:"note_on_#{noteable}", project: project) }
it 'says that markdown and quick actions are supported' do
- expect(rendered).to have_content('Markdown and quick actions are supported')
+ expect(rendered).to have_content('Supports Markdown. For quick actions, type /.')
end
end
end
diff --git a/workhorse/internal/redis/keywatcher.go b/workhorse/internal/redis/keywatcher.go
index 13e9fc3f051..82cb082f5f0 100644
--- a/workhorse/internal/redis/keywatcher.go
+++ b/workhorse/internal/redis/keywatcher.go
@@ -1,6 +1,7 @@
package redis
import (
+ "errors"
"fmt"
"strings"
"sync"
@@ -189,7 +190,9 @@ func WatchKey(key, value string, timeout time.Duration) (WatchKeyStatus, error)
defer delKeyChan(kw)
currentValue, err := GetString(key)
- if err != nil {
+ if errors.Is(err, redis.ErrNil) {
+ currentValue = ""
+ } else if err != nil {
return WatchKeyStatusNoChange, fmt.Errorf("keywatcher: redis GET: %v", err)
}
if currentValue != value {
diff --git a/workhorse/internal/redis/keywatcher_test.go b/workhorse/internal/redis/keywatcher_test.go
index 7ff5f8204c0..a2f2b73898f 100644
--- a/workhorse/internal/redis/keywatcher_test.go
+++ b/workhorse/internal/redis/keywatcher_test.go
@@ -1,10 +1,12 @@
package redis
import (
+ "errors"
"sync"
"testing"
"time"
+ "github.com/gomodule/redigo/redis"
"github.com/rafaeljusto/redigomock"
"github.com/stretchr/testify/require"
)
@@ -65,100 +67,191 @@ func processMessages(numWatchers int, value string) {
processInner(psc)
}
-func TestWatchKeySeenChange(t *testing.T) {
+type keyChangeTestCase struct {
+ desc string
+ returnValue string
+ isKeyMissing bool
+ watchValue string
+ processedValue string
+ expectedStatus WatchKeyStatus
+ timeout time.Duration
+}
+
+func TestKeyChangesBubblesUpError(t *testing.T) {
conn, td := setupMockPool()
defer td()
- conn.Command("GET", runnerKey).Expect("something")
-
- wg := &sync.WaitGroup{}
- wg.Add(1)
+ conn.Command("GET", runnerKey).ExpectError(errors.New("test error"))
- go func() {
- val, err := WatchKey(runnerKey, "something", time.Second)
- require.NoError(t, err, "Expected no error")
- require.Equal(t, WatchKeyStatusSeenChange, val, "Expected value to change")
- wg.Done()
- }()
+ _, err := WatchKey(runnerKey, "something", time.Second)
+ require.Error(t, err, "Expected error")
- processMessages(1, "somethingelse")
- wg.Wait()
+ deleteWatchers(runnerKey)
}
-func TestWatchKeyNoChange(t *testing.T) {
- conn, td := setupMockPool()
- defer td()
+func TestKeyChangesInstantReturn(t *testing.T) {
+ testCases := []keyChangeTestCase{
+ // WatchKeyStatusAlreadyChanged
+ {
+ desc: "sees change with key existing and changed",
+ returnValue: "somethingelse",
+ watchValue: "something",
+ expectedStatus: WatchKeyStatusAlreadyChanged,
+ timeout: time.Second,
+ },
+ {
+ desc: "sees change with key non-existing",
+ isKeyMissing: true,
+ watchValue: "something",
+ processedValue: "somethingelse",
+ expectedStatus: WatchKeyStatusAlreadyChanged,
+ timeout: time.Second,
+ },
+ // WatchKeyStatusTimeout
+ {
+ desc: "sees timeout with key existing and unchanged",
+ returnValue: "something",
+ watchValue: "something",
+ expectedStatus: WatchKeyStatusTimeout,
+ timeout: time.Millisecond,
+ },
+ {
+ desc: "sees timeout with key non-existing and unchanged",
+ isKeyMissing: true,
+ watchValue: "",
+ expectedStatus: WatchKeyStatusTimeout,
+ timeout: time.Millisecond,
+ },
+ }
- conn.Command("GET", runnerKey).Expect("something")
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ conn, td := setupMockPool()
+ defer td()
- wg := &sync.WaitGroup{}
- wg.Add(1)
+ if tc.isKeyMissing {
+ conn.Command("GET", runnerKey).ExpectError(redis.ErrNil)
+ } else {
+ conn.Command("GET", runnerKey).Expect(tc.returnValue)
+ }
- go func() {
- val, err := WatchKey(runnerKey, "something", time.Second)
- require.NoError(t, err, "Expected no error")
- require.Equal(t, WatchKeyStatusNoChange, val, "Expected notification without change to value")
- wg.Done()
- }()
+ val, err := WatchKey(runnerKey, tc.watchValue, tc.timeout)
- processMessages(1, "something")
- wg.Wait()
-}
+ require.NoError(t, err, "Expected no error")
+ require.Equal(t, tc.expectedStatus, val, "Expected value")
-func TestWatchKeyTimeout(t *testing.T) {
- conn, td := setupMockPool()
- defer td()
+ deleteWatchers(runnerKey)
+ })
+ }
+}
- conn.Command("GET", runnerKey).Expect("something")
+func TestKeyChangesWhenWatching(t *testing.T) {
+ testCases := []keyChangeTestCase{
+ // WatchKeyStatusSeenChange
+ {
+ desc: "sees change with key existing",
+ returnValue: "something",
+ watchValue: "something",
+ processedValue: "somethingelse",
+ expectedStatus: WatchKeyStatusSeenChange,
+ },
+ {
+ desc: "sees change with key non-existing, when watching empty value",
+ isKeyMissing: true,
+ watchValue: "",
+ processedValue: "something",
+ expectedStatus: WatchKeyStatusSeenChange,
+ },
+ // WatchKeyStatusNoChange
+ {
+ desc: "sees no change with key existing",
+ returnValue: "something",
+ watchValue: "something",
+ processedValue: "something",
+ expectedStatus: WatchKeyStatusNoChange,
+ },
+ }
- val, err := WatchKey(runnerKey, "something", time.Millisecond)
- require.NoError(t, err, "Expected no error")
- require.Equal(t, WatchKeyStatusTimeout, val, "Expected value to not change")
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ conn, td := setupMockPool()
+ defer td()
- // Clean up watchers since Process isn't doing that for us (not running)
- deleteWatchers(runnerKey)
-}
+ if tc.isKeyMissing {
+ conn.Command("GET", runnerKey).ExpectError(redis.ErrNil)
+ } else {
+ conn.Command("GET", runnerKey).Expect(tc.returnValue)
+ }
-func TestWatchKeyAlreadyChanged(t *testing.T) {
- conn, td := setupMockPool()
- defer td()
+ wg := &sync.WaitGroup{}
+ wg.Add(1)
- conn.Command("GET", runnerKey).Expect("somethingelse")
+ go func() {
+ defer wg.Done()
+ val, err := WatchKey(runnerKey, tc.watchValue, time.Second)
- val, err := WatchKey(runnerKey, "something", time.Second)
- require.NoError(t, err, "Expected no error")
- require.Equal(t, WatchKeyStatusAlreadyChanged, val, "Expected value to have already changed")
+ require.NoError(t, err, "Expected no error")
+ require.Equal(t, tc.expectedStatus, val, "Expected value")
+ }()
- // Clean up watchers since Process isn't doing that for us (not running)
- deleteWatchers(runnerKey)
+ processMessages(1, tc.processedValue)
+ wg.Wait()
+ })
+ }
}
-func TestWatchKeyMassivelyParallel(t *testing.T) {
- runTimes := 100 // 100 parallel watchers
+func TestKeyChangesParallel(t *testing.T) {
+ testCases := []keyChangeTestCase{
+ {
+ desc: "massively parallel, sees change with key existing",
+ returnValue: "something",
+ watchValue: "something",
+ processedValue: "somethingelse",
+ expectedStatus: WatchKeyStatusSeenChange,
+ },
+ {
+ desc: "massively parallel, sees change with key existing, watching missing keys",
+ isKeyMissing: true,
+ watchValue: "",
+ processedValue: "somethingelse",
+ expectedStatus: WatchKeyStatusSeenChange,
+ },
+ }
- conn, td := setupMockPool()
- defer td()
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ runTimes := 100
- wg := &sync.WaitGroup{}
- wg.Add(runTimes)
+ conn, td := setupMockPool()
+ defer td()
- getCmd := conn.Command("GET", runnerKey)
+ getCmd := conn.Command("GET", runnerKey)
- for i := 0; i < runTimes; i++ {
- getCmd = getCmd.Expect("something")
- }
+ for i := 0; i < runTimes; i++ {
+ if tc.isKeyMissing {
+ getCmd = getCmd.ExpectError(redis.ErrNil)
+ } else {
+ getCmd = getCmd.Expect(tc.returnValue)
+ }
+ }
- for i := 0; i < runTimes; i++ {
- go func() {
- val, err := WatchKey(runnerKey, "something", time.Second)
- require.NoError(t, err, "Expected no error")
- require.Equal(t, WatchKeyStatusSeenChange, val, "Expected value to change")
- wg.Done()
- }()
- }
+ wg := &sync.WaitGroup{}
+ wg.Add(runTimes)
- processMessages(runTimes, "somethingelse")
- wg.Wait()
+ for i := 0; i < runTimes; i++ {
+ go func() {
+ defer wg.Done()
+ val, err := WatchKey(runnerKey, tc.watchValue, time.Second)
+
+ require.NoError(t, err, "Expected no error")
+ require.Equal(t, tc.expectedStatus, val, "Expected value")
+ }()
+ }
+
+ processMessages(runTimes, tc.processedValue)
+ wg.Wait()
+ })
+ }
}
func TestShutdown(t *testing.T) {