Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-17 12:08:06 +00:00
parent 91c2554bcf
commit f0a387b4a5
41 changed files with 944 additions and 255 deletions

View File

@ -1 +1 @@
42fab8fc526215f9426bc9f459f9e6da0951c574
6b31501b13eae70aea5061edc8273c551ba4c349

View File

@ -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"

View File

@ -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) {

View File

@ -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);
};

View File

@ -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"
/>

View File

@ -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.`),

View File

@ -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;

View File

@ -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.
*

View File

@ -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,

View File

@ -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) {

View File

@ -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>

View File

@ -118,3 +118,11 @@ a {
}
}
}
.tanuki-logo {
width: 210px;
height: 210px;
max-width: 40vw;
display: block;
margin: map-get($spacing-scale, 4) auto;
}

View File

@ -1,10 +1,46 @@
@import 'mixins_and_variables_and_functions';
.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 {
padding-inline-start: 2.5rem;
.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;
}

View File

@ -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

View File

@ -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

View File

@ -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();

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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!
```

View File

@ -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:

View File

@ -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:

View File

@ -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.

View File

@ -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/)

View File

@ -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.

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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);
});
});
});

View File

@ -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

View File

@ -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);
});
});
});

View File

@ -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',
});
});

View File

@ -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);
});
});
});

View File

@ -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,24 +469,47 @@ 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 match_array(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
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
describe '#deprecation_warnings' do
@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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) {
conn, td := setupMockPool()
defer td()
conn.Command("GET", runnerKey).Expect("something")
wg := &sync.WaitGroup{}
wg.Add(1)
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()
}()
processMessages(1, "somethingelse")
wg.Wait()
type keyChangeTestCase struct {
desc string
returnValue string
isKeyMissing bool
watchValue string
processedValue string
expectedStatus WatchKeyStatus
timeout time.Duration
}
func TestWatchKeyNoChange(t *testing.T) {
func TestKeyChangesBubblesUpError(t *testing.T) {
conn, td := setupMockPool()
defer td()
conn.Command("GET", runnerKey).Expect("something")
conn.Command("GET", runnerKey).ExpectError(errors.New("test error"))
wg := &sync.WaitGroup{}
wg.Add(1)
_, err := WatchKey(runnerKey, "something", time.Second)
require.Error(t, err, "Expected error")
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()
}()
processMessages(1, "something")
wg.Wait()
}
func TestWatchKeyTimeout(t *testing.T) {
conn, td := setupMockPool()
defer td()
conn.Command("GET", runnerKey).Expect("something")
val, err := WatchKey(runnerKey, "something", time.Millisecond)
require.NoError(t, err, "Expected no error")
require.Equal(t, WatchKeyStatusTimeout, val, "Expected value to not change")
// Clean up watchers since Process isn't doing that for us (not running)
deleteWatchers(runnerKey)
}
func TestWatchKeyAlreadyChanged(t *testing.T) {
conn, td := setupMockPool()
defer td()
conn.Command("GET", runnerKey).Expect("somethingelse")
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")
// Clean up watchers since Process isn't doing that for us (not running)
deleteWatchers(runnerKey)
}
func TestWatchKeyMassivelyParallel(t *testing.T) {
runTimes := 100 // 100 parallel watchers
conn, td := setupMockPool()
defer td()
wg := &sync.WaitGroup{}
wg.Add(runTimes)
getCmd := conn.Command("GET", runnerKey)
for i := 0; i < runTimes; i++ {
getCmd = getCmd.Expect("something")
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,
},
}
for i := 0; i < runTimes; i++ {
go func() {
val, err := WatchKey(runnerKey, "something", time.Second)
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
conn, td := setupMockPool()
defer td()
if tc.isKeyMissing {
conn.Command("GET", runnerKey).ExpectError(redis.ErrNil)
} else {
conn.Command("GET", runnerKey).Expect(tc.returnValue)
}
val, err := WatchKey(runnerKey, tc.watchValue, tc.timeout)
require.NoError(t, err, "Expected no error")
require.Equal(t, WatchKeyStatusSeenChange, val, "Expected value to change")
wg.Done()
}()
require.Equal(t, tc.expectedStatus, val, "Expected value")
deleteWatchers(runnerKey)
})
}
}
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,
},
}
processMessages(runTimes, "somethingelse")
wg.Wait()
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
conn, td := setupMockPool()
defer td()
if tc.isKeyMissing {
conn.Command("GET", runnerKey).ExpectError(redis.ErrNil)
} else {
conn.Command("GET", runnerKey).Expect(tc.returnValue)
}
wg := &sync.WaitGroup{}
wg.Add(1)
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(1, tc.processedValue)
wg.Wait()
})
}
}
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,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
runTimes := 100
conn, td := setupMockPool()
defer td()
getCmd := conn.Command("GET", runnerKey)
for i := 0; i < runTimes; i++ {
if tc.isKeyMissing {
getCmd = getCmd.ExpectError(redis.ErrNil)
} else {
getCmd = getCmd.Expect(tc.returnValue)
}
}
wg := &sync.WaitGroup{}
wg.Add(runTimes)
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) {