Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-11-08 15:13:35 +00:00
parent 6a38034714
commit 05db4ead6d
79 changed files with 1189 additions and 312 deletions

View File

@ -534,6 +534,50 @@ rspec:feature-flags:
run_timed_command "bundle exec scripts/used-feature-flags"; run_timed_command "bundle exec scripts/used-feature-flags";
fi fi
rspec:skipped-flaky-tests-report:
extends:
- .default-retry
- .rails:rules:skipped-flaky-tests-report
image: ruby:2.7-alpine
stage: post-test
# We cannot use needs since it would mean needing 84 jobs (since most are parallelized)
# so we use `dependencies` here.
dependencies:
# FOSS/EE jobs
- rspec migration pg12
- rspec unit pg12
- rspec integration pg12
- rspec system pg12
# FOSS/EE minimal jobs
- rspec migration pg12 minimal
- rspec unit pg12 minimal
- rspec integration pg12 minimal
- rspec system pg12 minimal
# EE jobs
- rspec-ee migration pg12
- rspec-ee unit pg12
- rspec-ee integration pg12
- rspec-ee system pg12
# EE minimal jobs
- rspec-ee migration pg12 minimal
- rspec-ee unit pg12 minimal
- rspec-ee integration pg12 minimal
- rspec-ee system pg12 minimal
# Geo jobs
- rspec-ee unit pg12 geo
- rspec-ee integration pg12 geo
- rspec-ee system pg12 geo
# Geo minimal jobs
- rspec-ee unit pg12 geo minimal
- rspec-ee integration pg12 geo minimal
- rspec-ee system pg12 geo minimal
script:
- cat rspec_flaky/skipped_flaky_tests_*_report.txt >> skipped_flaky_tests_report.txt
artifacts:
expire_in: 31d
paths:
- skipped_flaky_tests_report.txt
# EE/FOSS: default refs (MRs, default branch, schedules) jobs # # EE/FOSS: default refs (MRs, default branch, schedules) jobs #
####################################################### #######################################################

View File

@ -1352,6 +1352,13 @@
when: never when: never
- changes: *code-backstage-patterns - changes: *code-backstage-patterns
.rails:rules:skipped-flaky-tests-report:
rules:
- <<: *if-not-ee
when: never
- if: '$SKIP_FLAKY_TESTS_AUTOMATICALLY == "true"'
changes: *code-backstage-patterns
######################### #########################
# Static analysis rules # # Static analysis rules #
######################### #########################

View File

@ -1 +1 @@
7b9cd199b0851fd1b6615e0798f2aafddafd63cb 460a880c6993ab5f76cac951fccc02efd5cbd444

View File

@ -342,7 +342,7 @@ group :development do
gem 'lefthook', '~> 0.7.0', require: false gem 'lefthook', '~> 0.7.0', require: false
gem 'solargraph', '~> 0.43', require: false gem 'solargraph', '~> 0.43', require: false
gem 'letter_opener_web', '~> 1.4.1' gem 'letter_opener_web', '~> 2.0.0'
# Better errors handler # Better errors handler
gem 'better_errors', '~> 2.9.0' gem 'better_errors', '~> 2.9.0'

View File

@ -700,10 +700,11 @@ GEM
lefthook (0.7.5) lefthook (0.7.5)
letter_opener (1.7.0) letter_opener (1.7.0)
launchy (~> 2.2) launchy (~> 2.2)
letter_opener_web (1.4.1) letter_opener_web (2.0.0)
actionmailer (>= 3.2) actionmailer (>= 5.2)
letter_opener (~> 1.0) letter_opener (~> 1.7)
railties (>= 3.2) railties (>= 5.2)
rexml
libyajl2 (1.2.0) libyajl2 (1.2.0)
license_finder (6.0.0) license_finder (6.0.0)
bundler bundler
@ -1516,7 +1517,7 @@ DEPENDENCIES
kramdown (~> 2.3.1) kramdown (~> 2.3.1)
kubeclient (~> 4.9.2) kubeclient (~> 4.9.2)
lefthook (~> 0.7.0) lefthook (~> 0.7.0)
letter_opener_web (~> 1.4.1) letter_opener_web (~> 2.0.0)
license_finder (~> 6.0) license_finder (~> 6.0)
licensee (~> 9.14.1) licensee (~> 9.14.1)
lockbox (~> 0.6.2) lockbox (~> 0.6.2)

View File

@ -1,10 +0,0 @@
query GroupBoardIterations($fullPath: ID!, $title: String) {
group(fullPath: $fullPath) {
iterations(includeAncestors: true, title: $title) {
nodes {
id
title
}
}
}
}

View File

@ -1,10 +0,0 @@
query ProjectBoardIterations($fullPath: ID!, $title: String) {
project(fullPath: $fullPath) {
iterations(includeAncestors: true, title: $title) {
nodes {
id
title
}
}
}
}

View File

@ -36,13 +36,11 @@ import {
} from '../boards_util'; } from '../boards_util';
import { gqlClient } from '../graphql'; import { gqlClient } from '../graphql';
import boardLabelsQuery from '../graphql/board_labels.query.graphql'; import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupBoardIterationsQuery from '../graphql/group_board_iterations.query.graphql';
import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql'; import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql'; import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql'; import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql'; import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import projectBoardIterationsQuery from '../graphql/project_board_iterations.query.graphql';
import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql'; import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
import * as types from './mutation_types'; import * as types from './mutation_types';
@ -203,52 +201,6 @@ export default {
}); });
}, },
fetchIterations({ state, commit }, title) {
commit(types.RECEIVE_ITERATIONS_REQUEST);
const { fullPath, boardType } = state;
const variables = {
fullPath,
title,
};
let query;
if (boardType === BoardType.project) {
query = projectBoardIterationsQuery;
}
if (boardType === BoardType.group) {
query = groupBoardIterationsQuery;
}
if (!query) {
// eslint-disable-next-line @gitlab/require-i18n-strings
throw new Error('Unknown board type');
}
return gqlClient
.query({
query,
variables,
})
.then(({ data }) => {
const errors = data[boardType]?.errors;
const iterations = data[boardType]?.iterations.nodes;
if (errors?.[0]) {
throw new Error(errors[0]);
}
commit(types.RECEIVE_ITERATIONS_SUCCESS, iterations);
return iterations;
})
.catch((e) => {
commit(types.RECEIVE_ITERATIONS_FAILURE);
throw e;
});
},
fetchMilestones({ state, commit }, searchTerm) { fetchMilestones({ state, commit }, searchTerm) {
commit(types.RECEIVE_MILESTONES_REQUEST); commit(types.RECEIVE_MILESTONES_REQUEST);

View File

@ -41,7 +41,3 @@ export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS'; export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION'; export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
export const SET_ERROR = 'SET_ERROR'; export const SET_ERROR = 'SET_ERROR';
export const RECEIVE_ITERATIONS_REQUEST = 'RECEIVE_ITERATIONS_REQUEST';
export const RECEIVE_ITERATIONS_SUCCESS = 'RECEIVE_ITERATIONS_SUCCESS';
export const RECEIVE_ITERATIONS_FAILURE = 'RECEIVE_ITERATIONS_FAILURE';

View File

@ -64,20 +64,6 @@ export default {
); );
}, },
[mutationTypes.RECEIVE_ITERATIONS_REQUEST](state) {
state.iterationsLoading = true;
},
[mutationTypes.RECEIVE_ITERATIONS_SUCCESS](state, iterations) {
state.iterations = iterations;
state.iterationsLoading = false;
},
[mutationTypes.RECEIVE_ITERATIONS_FAILURE](state) {
state.iterationsLoading = false;
state.error = __('Failed to load iterations.');
},
[mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) { [mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
state.activeId = id; state.activeId = id;
state.sidebarType = sidebarType; state.sidebarType = sidebarType;

View File

@ -44,6 +44,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE, TRACKING_MULTIPLE_FILES_MODE,
} from '../constants'; } from '../constants';
import { discussionIntersectionObserverHandlerFactory } from '../utils/discussions';
import diffsEventHub from '../event_hub'; import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews'; import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance'; import { diffsApp } from '../utils/performance';
@ -86,6 +87,9 @@ export default {
ALERT_MERGE_CONFLICT, ALERT_MERGE_CONFLICT,
ALERT_COLLAPSED_FILES, ALERT_COLLAPSED_FILES,
}, },
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
props: { props: {
endpoint: { endpoint: {
type: String, type: String,

View File

@ -0,0 +1,76 @@
function normalize(processable) {
const { entry } = processable;
const offset = entry.rootBounds.bottom - entry.boundingClientRect.top;
const direction =
offset < 0 ? 'Up' : 'Down'; /* eslint-disable-line @gitlab/require-i18n-strings */
return {
...processable,
entry: {
time: entry.time,
type: entry.isIntersecting ? 'intersection' : `scroll${direction}`,
},
};
}
function sort({ entry: alpha }, { entry: beta }) {
const diff = alpha.time - beta.time;
let order = 0;
if (diff < 0) {
order = -1;
} else if (diff > 0) {
order = 1;
} else if (alpha.type === 'intersection' && beta.type === 'scrollUp') {
order = 2;
} else if (alpha.type === 'scrollUp' && beta.type === 'intersection') {
order = -2;
}
return order;
}
function filter(entry) {
return entry.type !== 'scrollDown';
}
export function discussionIntersectionObserverHandlerFactory() {
let unprocessed = [];
let timer = null;
return (processable) => {
unprocessed.push(processable);
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
unprocessed
.map(normalize)
.filter(filter)
.sort(sort)
.forEach((discussionObservationContainer) => {
const {
entry: { type },
currentDiscussion,
isFirstUnresolved,
isDiffsPage,
functions: { setCurrentDiscussionId, getPreviousUnresolvedDiscussionId },
} = discussionObservationContainer;
if (type === 'intersection') {
setCurrentDiscussionId(currentDiscussion.id);
} else if (type === 'scrollUp') {
setCurrentDiscussionId(
isFirstUnresolved
? null
: getPreviousUnresolvedDiscussionId(currentDiscussion.id, isDiffsPage),
);
}
});
unprocessed = [];
}, 0);
};
}

View File

@ -1,5 +1,6 @@
<script> <script>
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import { GlIntersectionObserver } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue'; import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue'; import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
@ -16,7 +17,9 @@ export default {
ToggleRepliesWidget, ToggleRepliesWidget,
NoteEditedText, NoteEditedText,
DiscussionNotesRepliesWrapper, DiscussionNotesRepliesWrapper,
GlIntersectionObserver,
}, },
inject: ['discussionObserverHandler'],
props: { props: {
discussion: { discussion: {
type: Object, type: Object,
@ -54,7 +57,11 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters(['userCanReply']), ...mapGetters([
'userCanReply',
'previousUnresolvedDiscussionId',
'firstUnresolvedDiscussionId',
]),
hasReplies() { hasReplies() {
return Boolean(this.replies.length); return Boolean(this.replies.length);
}, },
@ -77,9 +84,20 @@ export default {
url: this.discussion.discussion_path, url: this.discussion.discussion_path,
}; };
}, },
isFirstUnresolved() {
return this.firstUnresolvedDiscussionId === this.discussion.id;
},
},
observerOptions: {
threshold: 0,
rootMargin: '0px 0px -50% 0px',
}, },
methods: { methods: {
...mapActions(['toggleDiscussion', 'setSelectedCommentPositionHover']), ...mapActions([
'toggleDiscussion',
'setSelectedCommentPositionHover',
'setCurrentDiscussionId',
]),
componentName(note) { componentName(note) {
if (note.isPlaceholderNote) { if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) { if (note.placeholderType === SYSTEM_NOTE) {
@ -110,6 +128,18 @@ export default {
this.setSelectedCommentPositionHover(); this.setSelectedCommentPositionHover();
} }
}, },
observerTriggered(entry) {
this.discussionObserverHandler({
entry,
isFirstUnresolved: this.isFirstUnresolved,
currentDiscussion: { ...this.discussion },
isDiffsPage: !this.isOverviewTab,
functions: {
setCurrentDiscussionId: this.setCurrentDiscussionId,
getPreviousUnresolvedDiscussionId: this.previousUnresolvedDiscussionId,
},
});
},
}, },
}; };
</script> </script>
@ -122,33 +152,35 @@ export default {
@mouseleave="handleMouseLeave(discussion)" @mouseleave="handleMouseLeave(discussion)"
> >
<template v-if="shouldGroupReplies"> <template v-if="shouldGroupReplies">
<component <gl-intersection-observer :options="$options.observerOptions" @update="observerTriggered">
:is="componentName(firstNote)" <component
:note="componentData(firstNote)" :is="componentName(firstNote)"
:line="line || diffLine" :note="componentData(firstNote)"
:discussion-file="discussion.diff_file" :line="line || diffLine"
:commit="commit" :discussion-file="discussion.diff_file"
:help-page-path="helpPagePath" :commit="commit"
:show-reply-button="userCanReply" :help-page-path="helpPagePath"
:discussion-root="true" :show-reply-button="userCanReply"
:discussion-resolve-path="discussion.resolve_path" :discussion-root="true"
:is-overview-tab="isOverviewTab" :discussion-resolve-path="discussion.resolve_path"
@handleDeleteNote="$emit('deleteNote')" :is-overview-tab="isOverviewTab"
@startReplying="$emit('startReplying')" @handleDeleteNote="$emit('deleteNote')"
> @startReplying="$emit('startReplying')"
<template #discussion-resolved-text> >
<note-edited-text <template #discussion-resolved-text>
v-if="discussion.resolved" <note-edited-text
:edited-at="discussion.resolved_at" v-if="discussion.resolved"
:edited-by="discussion.resolved_by" :edited-at="discussion.resolved_at"
:action-text="resolvedText" :edited-by="discussion.resolved_by"
class-name="discussion-headline-light js-discussion-headline discussion-resolved-text" :action-text="resolvedText"
/> class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
</template> />
<template #avatar-badge> </template>
<slot name="avatar-badge"></slot> <template #avatar-badge>
</template> <slot name="avatar-badge"></slot>
</component> </template>
</component>
</gl-intersection-observer>
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion"> <discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
<toggle-replies-widget <toggle-replies-widget
v-if="hasReplies" v-if="hasReplies"

View File

@ -1,5 +1,5 @@
import initDevOpsScore from '~/analytics/devops_report/devops_score'; import initDevOpsScore from '~/analytics/devops_reports/devops_score';
import initDevOpsScoreDisabledServicePing from '~/analytics/devops_report/devops_score_disabled_service_ping'; import initDevOpsScoreDisabledServicePing from '~/analytics/devops_reports/devops_score_disabled_service_ping';
initDevOpsScoreDisabledServicePing(); initDevOpsScoreDisabledServicePing();
initDevOpsScore(); initDevOpsScore();

View File

@ -227,7 +227,7 @@
// IMPORTANT PERFORMANCE OPTIMIZATION // IMPORTANT PERFORMANCE OPTIMIZATION
// //
// When viewinng a blame with many commits a lot of content is rendered on the page. // When viewinng a blame with many commits a lot of content is rendered on the page.
// Two selectors below ensure that we only render what is visible to the user, thus reducing TBT in the browser. // content-visibility rules below ensure that we only render what is visible to the user, thus reducing TBT in the browser.
.commit { .commit {
content-visibility: auto; content-visibility: auto;
contain-intrinsic-size: 1px 3em; contain-intrinsic-size: 1px 3em;
@ -237,6 +237,10 @@
content-visibility: auto; content-visibility: auto;
contain-intrinsic-size: 1px 1.1875rem; contain-intrinsic-size: 1px 1.1875rem;
} }
.line-numbers {
content-visibility: auto;
}
} }
&.logs { &.logs {

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module IssuesHelper module IssuesHelper
include Issues::IssueTypeHelpers
def issue_css_classes(issue) def issue_css_classes(issue)
classes = ["issue"] classes = ["issue"]
classes << "closed" if issue.closed? classes << "closed" if issue.closed?

View File

@ -41,8 +41,6 @@ module AlertEventLifecycle
scope :firing, -> { where(status: status_value_for(:firing)) } scope :firing, -> { where(status: status_value_for(:firing)) }
scope :resolved, -> { where(status: status_value_for(:resolved)) } scope :resolved, -> { where(status: status_value_for(:resolved)) }
scope :count_by_project_id, -> { group(:project_id).count }
def self.status_value_for(name) def self.status_value_for(name)
state_machines[:status].states[name].value state_machines[:status].states[name].value
end end

View File

@ -92,7 +92,6 @@ module Issuable
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_projects, ->(ids) { where(project_id: ids) }
scope :opened, -> { with_state(:opened) } scope :opened, -> { with_state(:opened) }
scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) } scope :closed, -> { with_state(:closed) }
# rubocop:disable GitlabSecurity/SqlInjection # rubocop:disable GitlabSecurity/SqlInjection

View File

@ -14,7 +14,6 @@ module Milestoneable
validate :milestone_is_valid validate :milestone_is_valid
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
scope :any_milestone, -> { where.not(milestone_id: nil) } scope :any_milestone, -> { where.not(milestone_id: nil) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) } scope :without_particular_milestone, ->(title) { left_outer_joins(:milestone).where("milestones.title != ? OR milestone_id IS NULL", title) }

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module AuthorizedProjectUpdate
class ProjectAccessChangedService
def initialize(project_ids)
@project_ids = Array.wrap(project_ids)
end
def execute(blocking: true)
bulk_args = @project_ids.map { |id| [id] }
if blocking
AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_and_wait(bulk_args)
else
AuthorizedProjectUpdate::ProjectRecalculateWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
end
end
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Issues
module IssueTypeHelpers
# @param object [Issue, Project]
# @param issue_type [String, Symbol]
def create_issue_type_allowed?(object, issue_type)
WorkItem::Type.base_types.key?(issue_type.to_s) &&
can?(current_user, :"create_#{issue_type}", object)
end
end
end

View File

@ -175,21 +175,18 @@ module Groups
end end
def refresh_project_authorizations def refresh_project_authorizations
ProjectAuthorization.where(project_id: @group.all_projects.select(:id)).delete_all # rubocop: disable CodeReuse/ActiveRecord projects_to_update = Set.new
# refresh authorized projects for current_user immediately # All projects in this hierarchy need to have their project authorizations recalculated
current_user.refresh_authorized_projects @group.all_projects.each_batch { |prjs| projects_to_update.merge(prjs.ids) } # rubocop: disable CodeReuse/ActiveRecord
# schedule refreshing projects for all the members of the group
@group.refresh_members_authorized_projects
# When a group is transferred, it also affects who gets access to the projects shared to # When a group is transferred, it also affects who gets access to the projects shared to
# the subgroups within its hierarchy, so we also schedule jobs that refresh authorizations for all such shared projects. # the subgroups within its hierarchy, so we also schedule jobs that refresh authorizations for all such shared projects.
project_group_shares_within_the_hierarchy = ProjectGroupLink.in_group(group.self_and_descendants.select(:id)) ProjectGroupLink.in_group(@group.self_and_descendants.select(:id)).each_batch do |project_group_links|
projects_to_update.merge(project_group_links.pluck(:project_id)) # rubocop: disable CodeReuse/ActiveRecord
project_group_shares_within_the_hierarchy.find_each do |project_group_link|
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project_group_link.project_id)
end end
AuthorizedProjectUpdate::ProjectAccessChangedService.new(projects_to_update.to_a).execute unless projects_to_update.empty?
end end
def raise_transfer_error(message) def raise_transfer_error(message)

View File

@ -3,6 +3,7 @@
module Issues module Issues
class BaseService < ::IssuableBaseService class BaseService < ::IssuableBaseService
include IncidentManagement::UsageData include IncidentManagement::UsageData
include IssueTypeHelpers
def hook_data(issue, action, old_associations: {}) def hook_data(issue, action, old_associations: {})
hook_data = issue.to_hook_data(current_user, old_associations: old_associations) hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
@ -44,7 +45,7 @@ module Issues
def filter_params(issue) def filter_params(issue)
super super
params.delete(:issue_type) unless issue_type_allowed?(issue) params.delete(:issue_type) unless create_issue_type_allowed?(issue, params[:issue_type])
filter_incident_label(issue) if params[:issue_type] filter_incident_label(issue) if params[:issue_type]
moved_issue = params.delete(:moved_issue) moved_issue = params.delete(:moved_issue)
@ -89,12 +90,6 @@ module Issues
Milestones::IssuesCountService.new(milestone).delete_cache Milestones::IssuesCountService.new(milestone).delete_cache
end end
# @param object [Issue, Project]
def issue_type_allowed?(object)
WorkItem::Type.base_types.key?(params[:issue_type]) &&
can?(current_user, :"create_#{params[:issue_type]}", object)
end
# @param issue [Issue] # @param issue [Issue]
def filter_incident_label(issue) def filter_incident_label(issue)
return unless add_incident_label?(issue) || remove_incident_label?(issue) return unless add_incident_label?(issue) || remove_incident_label?(issue)

View File

@ -80,7 +80,7 @@ module Issues
] ]
allowed_params << :milestone_id if can?(current_user, :admin_issue, project) allowed_params << :milestone_id if can?(current_user, :admin_issue, project)
allowed_params << :issue_type if issue_type_allowed?(project) allowed_params << :issue_type if create_issue_type_allowed?(project, params[:issue_type])
params.slice(*allowed_params) params.slice(*allowed_params)
end end

View File

@ -6,5 +6,5 @@
- if show_adoption? - if show_adoption?
= render_if_exists 'admin/dev_ops_report/devops_tabs' = render_if_exists 'admin/dev_ops_report/devops_tabs'
- else - else
= render 'report' = render 'score'

View File

@ -18,17 +18,19 @@
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon') = sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
.dropdown-content{ data: { testid: 'issue-type-select-dropdown' } } .dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
%ul %ul
%li.js-filter-issuable-type - if create_issue_type_allowed?(@project, :issue)
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do %li.js-filter-issuable-type
#{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_("Issue")} = link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
%li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } } #{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')}
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do - if create_issue_type_allowed?(@project, :incident)
#{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_("Incident")} %li.js-filter-issuable-type{ data: { track: { action: "select_issue_type_incident", label: "select_issue_type_incident_dropdown_option" } } }
= link_to new_project_issue_path(@project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }), class: ("is-active" if issuable.incident?) do
#{sprite_icon(work_item_type_icon(:incident), css_class: 'gl-icon')} #{_('Incident')}
#js-type-popover #js-type-popover
- if issuable.incident? - if issuable.incident?
%p.form-text.text-muted %p.form-text.text-muted
- incident_docs_url = help_page_path('operations/incident_management/incidents.md') - incident_docs_url = help_page_path('operations/incident_management/incidents.md')
- incident_docs_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: incident_docs_url } - incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url)
= _('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.').html_safe % { incident_docs_start: incident_docs_start, incident_docs_end: '</a>'.html_safe } = format(_('A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents.'), incident_docs_start: incident_docs_start, incident_docs_end: '</a>').html_safe

View File

@ -7,6 +7,8 @@ module AuthorizedProjectUpdate
data_consistency :always data_consistency :always
include Gitlab::ExclusiveLeaseHelpers include Gitlab::ExclusiveLeaseHelpers
prepend WaitableWorker
feature_category :authentication_and_authorization feature_category :authentication_and_authorization
urgency :high urgency :high
queue_namespace :authorized_project_update queue_namespace :authorized_project_update

View File

@ -159,7 +159,10 @@ module ContainerExpirationPolicies
return unless tags_count && cached_tags_count && tags_count != 0 return unless tags_count && cached_tags_count && tags_count != 0
log_extra_metadata_on_done(:cleanup_tags_service_cache_hit_ratio, cached_tags_count / tags_count.to_f) ratio = cached_tags_count / tags_count.to_f
ratio_as_percentage = (ratio * 100).round(2)
log_extra_metadata_on_done(:cleanup_tags_service_cache_hit_ratio, ratio_as_percentage)
end end
def log_truncate(result) def log_truncate(result)

View File

@ -16,6 +16,8 @@ Bundler.require(*Rails.groups)
module Gitlab module Gitlab
class Application < Rails::Application class Application < Rails::Application
config.load_defaults 6.1
require_dependency Rails.root.join('lib/gitlab') require_dependency Rails.root.join('lib/gitlab')
require_dependency Rails.root.join('lib/gitlab/utils') require_dependency Rails.root.join('lib/gitlab/utils')
require_dependency Rails.root.join('lib/gitlab/action_cable/config') require_dependency Rails.root.join('lib/gitlab/action_cable/config')
@ -37,8 +39,6 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/runtime') require_dependency Rails.root.join('lib/gitlab/runtime')
require_dependency Rails.root.join('lib/gitlab/patch/legacy_database_config') require_dependency Rails.root.join('lib/gitlab/patch/legacy_database_config')
config.autoloader = :zeitwerk
# To be removed in 15.0 # To be removed in 15.0
# This preload is needed to convert legacy `database.yml` # This preload is needed to convert legacy `database.yml`
# from `production: adapter: postgresql` # from `production: adapter: postgresql`
@ -190,11 +190,12 @@ module Gitlab
# regardless if schema_search_path is set, or not. # regardless if schema_search_path is set, or not.
config.active_record.dump_schemas = :all config.active_record.dump_schemas = :all
# Use new connection handling so that we can use Rails 6.1+ multiple # Override default Active Record settings
# database support. # We cannot do this in an initializer because some models are already loaded by then
config.active_record.legacy_connection_handling = false config.active_record.cache_versioning = false
config.active_record.collection_cache_versioning = false
config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" config.active_record.has_many_inversing = false
config.active_record.belongs_to_required_by_default = false
# Enable the asset pipeline # Enable the asset pipeline
config.assets.enabled = true config.assets.enabled = true
@ -380,6 +381,7 @@ module Gitlab
config.cache_store = :redis_cache_store, Gitlab::Redis::Cache.active_support_config config.cache_store = :redis_cache_store, Gitlab::Redis::Cache.active_support_config
config.active_job.queue_adapter = :sidekiq config.active_job.queue_adapter = :sidekiq
config.action_mailer.deliver_later_queue_name = :mailers
# This is needed for gitlab-shell # This is needed for gitlab-shell
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH'] ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']

View File

@ -1,7 +0,0 @@
# frozen_string_literal: true
# This file was introduced during upgrading Rails from 5.2 to 6.0.
# This file can be removed when `config.load_defaults 6.0` is introduced.
# Don't force requests from old versions of IE to be UTF-8 encoded.
Rails.application.config.action_view.default_enforce_utf8 = false

View File

@ -2,6 +2,5 @@
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
Rails.application.config.action_dispatch.use_cookies_with_metadata = true
Rails.application.config.action_dispatch.cookies_serializer = Rails.application.config.action_dispatch.cookies_serializer =
Gitlab::Utils.to_boolean(ENV['USE_UNSAFE_HYBRID_COOKIES']) ? :hybrid : :json Gitlab::Utils.to_boolean(ENV['USE_UNSAFE_HYBRID_COOKIES']) ? :hybrid : :json

View File

@ -1,24 +0,0 @@
# frozen_string_literal: true
# Remove this `if` condition when upgraded to rails 5.0.
# The body must be kept.
# Be sure to restart your server when you modify this file.
#
# This file contains migration options to ease your Rails 5.0 upgrade.
#
# Once upgraded flip defaults one by one to migrate to the new default.
#
# Read the Guide for Upgrading Ruby on Rails for more info on each option.
# Enable per-form CSRF tokens. Previous versions had false.
Rails.application.config.action_controller.per_form_csrf_tokens = false
# Enable origin-checking CSRF mitigation. Previous versions had false.
Rails.application.config.action_controller.forgery_protection_origin_check = false
# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`.
# Previous versions had false.
ActiveSupport.to_time_preserves_timezone = false
# Require `belongs_to` associations by default. Previous versions had false.
Rails.application.config.active_record.belongs_to_required_by_default = false

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
# This contains configuration from Rails upgrades to override the new defaults so that we
# keep existing behavior.
#
# For boolean values, the new default is the opposite of the value being set in this file.
# For other types, the new default is noted in the comments. These are also documented in
# https://guides.rubyonrails.org/configuring.html#results-of-config-load-defaults
#
# To switch a setting to the new default value, we just need to delete the specific line here.
Rails.application.configure do
# Rails 6.1
config.action_dispatch.cookies_same_site_protection = nil # New default is :lax
config.action_dispatch.ssl_default_redirect_status = nil # New default is 308
ActiveSupport.utc_to_local_returns_utc_offset_times = false
config.action_controller.urlsafe_csrf_tokens = false
config.action_view.preload_links_header = false
# Rails 5.2
config.action_dispatch.use_authenticated_cookie_encryption = false
config.active_support.use_authenticated_message_encryption = false
config.active_support.hash_digest_class = ::Digest::MD5 # New default is ::Digest::SHA1
config.action_controller.default_protect_from_forgery = false
config.action_view.form_with_generates_ids = false
# Rails 5.1
config.assets.unknown_asset_fallback = true
# Rails 5.0
config.action_controller.per_form_csrf_tokens = false
config.action_controller.forgery_protection_origin_check = false
ActiveSupport.to_time_preserves_timezone = false
config.ssl_options = {} # New default is { hsts: { subdomains: true } }
end

View File

@ -0,0 +1,112 @@
/* eslint-disable no-underscore-dangle */
const yaml = require('js-yaml');
const PLUGIN_NAME = 'GraphqlKnownOperationsPlugin';
const GRAPHQL_PATH_REGEX = /(query|mutation)\.graphql$/;
const OPERATION_NAME_SOURCE_REGEX = /^\s*module\.exports.*oneQuery.*"(\w+)"/gm;
/**
* Returns whether a given webpack module is a "graphql" module
*/
const isGraphqlModule = (module) => {
return GRAPHQL_PATH_REGEX.test(module.resource);
};
/**
* Returns graphql operation names we can parse from the given module
*
* Since webpack gives us the source **after** the graphql-tag/loader runs,
* we can look for specific lines we're guaranteed to have from the
* graphql-tag/loader.
*/
const getOperationNames = (module) => {
const originalSource = module.originalSource();
if (!originalSource) {
return [];
}
const matches = originalSource.source().toString().matchAll(OPERATION_NAME_SOURCE_REGEX);
return Array.from(matches).map((match) => match[1]);
};
const createFileContents = (knownOperations) => {
const sourceData = Array.from(knownOperations.values()).sort((a, b) => a.localeCompare(b));
return yaml.dump(sourceData);
};
/**
* Creates a webpack4 compatible "RawSource"
*
* Inspired from https://sourcegraph.com/github.com/FormidableLabs/webpack-stats-plugin@e050ff8c362d5ddd45c66ade724d4a397ace3e5c/-/blob/lib/stats-writer-plugin.js?L144
*/
const createWebpackRawSource = (source) => {
const buff = Buffer.from(source, 'utf-8');
return {
source() {
return buff;
},
size() {
return buff.length;
},
};
};
const onSucceedModule = ({ module, knownOperations }) => {
if (!isGraphqlModule(module)) {
return;
}
getOperationNames(module).forEach((x) => knownOperations.add(x));
};
const onCompilerEmit = ({ compilation, knownOperations, filename }) => {
const contents = createFileContents(knownOperations);
const source = createWebpackRawSource(contents);
const asset = compilation.getAsset(filename);
if (asset) {
compilation.updateAsset(filename, source);
} else {
compilation.emitAsset(filename, source);
}
};
/**
* Webpack plugin that outputs a file containing known graphql operations.
*
* A lot of the mechanices was expired from [this example][1].
*
* [1]: https://sourcegraph.com/github.com/FormidableLabs/webpack-stats-plugin@e050ff8c362d5ddd45c66ade724d4a397ace3e5c/-/blob/lib/stats-writer-plugin.js?L136
*/
class GraphqlKnownOperationsPlugin {
constructor({ filename }) {
this._filename = filename;
}
apply(compiler) {
const knownOperations = new Set();
compiler.hooks.emit.tap(PLUGIN_NAME, (compilation) => {
onCompilerEmit({
compilation,
knownOperations,
filename: this._filename,
});
});
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
compilation.hooks.succeedModule.tap(PLUGIN_NAME, (module) => {
onSucceedModule({
module,
knownOperations,
});
});
});
}
}
module.exports = GraphqlKnownOperationsPlugin;

View File

@ -24,6 +24,7 @@ const IS_JH = require('./helpers/is_jh_env');
const vendorDllHash = require('./helpers/vendor_dll_hash'); const vendorDllHash = require('./helpers/vendor_dll_hash');
const MonacoWebpackPlugin = require('./plugins/monaco_webpack'); const MonacoWebpackPlugin = require('./plugins/monaco_webpack');
const GraphqlKnownOperationsPlugin = require('./plugins/graphql_known_operations_plugin');
const ROOT_PATH = path.resolve(__dirname, '..'); const ROOT_PATH = path.resolve(__dirname, '..');
const SUPPORTED_BROWSERS = fs.readFileSync(path.join(ROOT_PATH, '.browserslistrc'), 'utf-8'); const SUPPORTED_BROWSERS = fs.readFileSync(path.join(ROOT_PATH, '.browserslistrc'), 'utf-8');
@ -456,6 +457,8 @@ module.exports = {
globalAPI: true, globalAPI: true,
}), }),
new GraphqlKnownOperationsPlugin({ filename: 'graphql_known_operations.yml' }),
// fix legacy jQuery plugins which depend on globals // fix legacy jQuery plugins which depend on globals
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
$: 'jquery', $: 'jquery',

View File

@ -1298,6 +1298,7 @@ docker build:
- `rules: changes` works the same way as [`only: changes` and `except: changes`](#onlychanges--exceptchanges). - `rules: changes` works the same way as [`only: changes` and `except: changes`](#onlychanges--exceptchanges).
- You can use `when: never` to implement a rule similar to [`except:changes`](#onlychanges--exceptchanges). - You can use `when: never` to implement a rule similar to [`except:changes`](#onlychanges--exceptchanges).
- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
#### `rules:exists` #### `rules:exists`
@ -1330,6 +1331,7 @@ job:
file paths. After the 10,000th check, rules with patterned globs always match. file paths. After the 10,000th check, rules with patterned globs always match.
In other words, the `exists` rule always assumes a match in projects with more In other words, the `exists` rule always assumes a match in projects with more
than 10,000 files. than 10,000 files.
- `exists` resolves to `true` if any of the listed files are found (an `OR` operation).
#### `rules:allow_failure` #### `rules:allow_failure`
@ -1567,7 +1569,7 @@ docker build:
**Additional details**: **Additional details**:
- If any of the matching files are changed (an `OR` operation), `changes` resolves to `true`. - `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
- If you use refs other than `branches`, `external_pull_requests`, or `merge_requests`, - If you use refs other than `branches`, `external_pull_requests`, or `merge_requests`,
`changes` can't determine if a given file is new or old and always returns `true`. `changes` can't determine if a given file is new or old and always returns `true`.
- If you use `only: changes` with other refs, jobs ignore the changes and always run. - If you use `only: changes` with other refs, jobs ignore the changes and always run.

View File

@ -166,6 +166,13 @@ Our current RSpec tests parallelization setup is as follows:
After that, the next pipeline uses the up-to-date `knapsack/report-master.json` file. After that, the next pipeline uses the up-to-date `knapsack/report-master.json` file.
### Flaky tests
Tests that are [known to be flaky](testing_guide/flaky_tests.md#automatic-retries-and-flaky-tests-detection) are:
- skipped if the `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is set to `true` (`false` by default)
- run if `$SKIP_FLAKY_TESTS_AUTOMATICALLY` variable is not set to `true` or if the `~"pipeline:run-flaky-tests"` label is set on the MR
### Monitoring ### Monitoring
The GitLab test suite is [monitored](performance.md#rspec-profiling) for the `main` branch, and any branch The GitLab test suite is [monitored](performance.md#rspec-profiling) for the `main` branch, and any branch

View File

@ -85,6 +85,7 @@ the Agent in subsequent steps.
In GitLab: In GitLab:
1. Ensure that [GitLab CI/CD is enabled in your project](../../../../ci/enable_or_disable_ci.md#enable-cicd-in-a-project).
1. From your project's sidebar, select **Infrastructure > Kubernetes clusters**. 1. From your project's sidebar, select **Infrastructure > Kubernetes clusters**.
1. Select the **GitLab Agent managed clusters** tab. 1. Select the **GitLab Agent managed clusters** tab.
1. Select **Integrate with the GitLab Agent**. 1. Select **Integrate with the GitLab Agent**.

View File

@ -29,13 +29,12 @@ Learn more about how GitLab can help you run [Infrastructure as Code](iac/index.
## Integrated Kubernetes management ## Integrated Kubernetes management
GitLab has special integrations with Kubernetes to help you deploy, manage and troubleshoot The GitLab integration with Kubernetes helps you to install, configure, manage, deploy, and troubleshoot
third-party or custom applications in Kubernetes clusters. Auto DevOps provides a full cluster applications. With the GitLab Kubernetes Agent, you can connect clusters behind a firewall,
DevSecOps pipeline by default targeted at Kubernetes based deployments. To support have real-time access to API endpoints, perform pull-beased or push-based deployments for production
all the GitLab features, GitLab offers a cluster management project for easy onboarding. and non-production environments, and much more.
The deploy boards provide quick insights into your cluster, including pod logs tailing.
Learn more about the [GitLab integration with Kubernetes](clusters/index.md). Learn more about the [GitLab Kubernetes Agent](../clusters/agent/index.md).
## Runbooks in GitLab ## Runbooks in GitLab

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
module Gitlab
module Graphql
class KnownOperations
Operation = Struct.new(:name) do
def to_caller_id
"graphql:#{name}"
end
end
ANONYMOUS = Operation.new("anonymous").freeze
UNKNOWN = Operation.new("unknown").freeze
def self.default
@default ||= self.new(Gitlab::Webpack::GraphqlKnownOperations.load)
end
def initialize(operation_names)
@operation_hash = operation_names
.map { |name| Operation.new(name).freeze }
.concat([ANONYMOUS, UNKNOWN])
.index_by(&:name)
end
# Returns the known operation from the given ::GraphQL::Query object
def from_query(query)
operation_name = query.selected_operation_name
return ANONYMOUS unless operation_name
@operation_hash[operation_name] || UNKNOWN
end
def operations
@operation_hash.values
end
end
end
end

View File

@ -6,11 +6,13 @@ module Gitlab
module SidekiqConfig module SidekiqConfig
FOSS_QUEUE_CONFIG_PATH = 'app/workers/all_queues.yml' FOSS_QUEUE_CONFIG_PATH = 'app/workers/all_queues.yml'
EE_QUEUE_CONFIG_PATH = 'ee/app/workers/all_queues.yml' EE_QUEUE_CONFIG_PATH = 'ee/app/workers/all_queues.yml'
JH_QUEUE_CONFIG_PATH = 'jh/app/workers/all_queues.yml'
SIDEKIQ_QUEUES_PATH = 'config/sidekiq_queues.yml' SIDEKIQ_QUEUES_PATH = 'config/sidekiq_queues.yml'
QUEUE_CONFIG_PATHS = [ QUEUE_CONFIG_PATHS = [
FOSS_QUEUE_CONFIG_PATH, FOSS_QUEUE_CONFIG_PATH,
(EE_QUEUE_CONFIG_PATH if Gitlab.ee?) (EE_QUEUE_CONFIG_PATH if Gitlab.ee?),
(JH_QUEUE_CONFIG_PATH if Gitlab.jh?)
].compact.freeze ].compact.freeze
# This maps workers not in our application code to queues. We need # This maps workers not in our application code to queues. We need
@ -33,7 +35,7 @@ module Gitlab
weight: 2, weight: 2,
tags: [] tags: []
) )
}.transform_values { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false) }.freeze }.transform_values { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: false, jh: false) }.freeze
class << self class << self
include Gitlab::SidekiqConfig::CliMethods include Gitlab::SidekiqConfig::CliMethods
@ -58,10 +60,14 @@ module Gitlab
@workers ||= begin @workers ||= begin
result = [] result = []
result.concat(DEFAULT_WORKERS.values) result.concat(DEFAULT_WORKERS.values)
result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false)) result.concat(find_workers(Rails.root.join('app', 'workers'), ee: false, jh: false))
if Gitlab.ee? if Gitlab.ee?
result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'), ee: true)) result.concat(find_workers(Rails.root.join('ee', 'app', 'workers'), ee: true, jh: false))
end
if Gitlab.jh?
result.concat(find_workers(Rails.root.join('jh', 'app', 'workers'), ee: false, jh: true))
end end
result result
@ -69,16 +75,26 @@ module Gitlab
end end
def workers_for_all_queues_yml def workers_for_all_queues_yml
workers.partition(&:ee?).reverse.map(&:sort) workers.each_with_object([[], [], []]) do |worker, array|
if worker.jh?
array[2].push(worker)
elsif worker.ee?
array[1].push(worker)
else
array[0].push(worker)
end
end.map(&:sort)
end end
# YAML.load_file is OK here as we control the file contents # YAML.load_file is OK here as we control the file contents
def all_queues_yml_outdated? def all_queues_yml_outdated?
foss_workers, ee_workers = workers_for_all_queues_yml foss_workers, ee_workers, jh_workers = workers_for_all_queues_yml
return true if foss_workers != YAML.load_file(FOSS_QUEUE_CONFIG_PATH) return true if foss_workers != YAML.load_file(FOSS_QUEUE_CONFIG_PATH)
Gitlab.ee? && ee_workers != YAML.load_file(EE_QUEUE_CONFIG_PATH) return true if Gitlab.ee? && ee_workers != YAML.load_file(EE_QUEUE_CONFIG_PATH)
Gitlab.jh? && File.exist?(JH_QUEUE_CONFIG_PATH) && jh_workers != YAML.load_file(JH_QUEUE_CONFIG_PATH)
end end
def queues_for_sidekiq_queues_yml def queues_for_sidekiq_queues_yml
@ -120,14 +136,14 @@ module Gitlab
private private
def find_workers(root, ee:) def find_workers(root, ee:, jh:)
concerns = root.join('concerns').to_s concerns = root.join('concerns').to_s
Dir[root.join('**', '*.rb')] Dir[root.join('**', '*.rb')]
.reject { |path| path.start_with?(concerns) } .reject { |path| path.start_with?(concerns) }
.map { |path| worker_from_path(path, root) } .map { |path| worker_from_path(path, root) }
.select { |worker| worker < Sidekiq::Worker } .select { |worker| worker < Sidekiq::Worker }
.map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: ee) } .map { |worker| Gitlab::SidekiqConfig::Worker.new(worker, ee: ee, jh: jh) }
end end
def worker_from_path(path, root) def worker_from_path(path, root)

View File

@ -18,6 +18,7 @@ module Gitlab
QUEUE_CONFIG_PATHS = begin QUEUE_CONFIG_PATHS = begin
result = %w[app/workers/all_queues.yml] result = %w[app/workers/all_queues.yml]
result << 'ee/app/workers/all_queues.yml' if Gitlab.ee? result << 'ee/app/workers/all_queues.yml' if Gitlab.ee?
result << 'jh/app/workers/all_queues.yml' if Gitlab.jh?
result result
end.freeze end.freeze

View File

@ -13,15 +13,20 @@ module Gitlab
:worker_has_external_dependencies?, :worker_has_external_dependencies?,
to: :klass to: :klass
def initialize(klass, ee:) def initialize(klass, ee:, jh: false)
@klass = klass @klass = klass
@ee = ee @ee = ee
@jh = jh
end end
def ee? def ee?
@ee @ee
end end
def jh?
@jh
end
def ==(other) def ==(other)
to_yaml == case other to_yaml == case other
when self.class when self.class

View File

@ -0,0 +1,65 @@
# frozen_string_literal: true
require 'net/http'
require 'uri'
module Gitlab
module Webpack
class FileLoader
class BaseError < StandardError
attr_reader :original_error, :uri
def initialize(uri, orig)
super orig.message
@uri = uri.to_s
@original_error = orig
end
end
StaticLoadError = Class.new(BaseError)
DevServerLoadError = Class.new(BaseError)
DevServerSSLError = Class.new(BaseError)
def self.load(path)
if Gitlab.config.webpack.dev_server.enabled
self.load_from_dev_server(path)
else
self.load_from_static(path)
end
end
def self.load_from_dev_server(path)
host = Gitlab.config.webpack.dev_server.host
port = Gitlab.config.webpack.dev_server.port
scheme = Gitlab.config.webpack.dev_server.https ? 'https' : 'http'
uri = Addressable::URI.new(scheme: scheme, host: host, port: port, path: self.dev_server_path(path))
# localhost could be blocked via Gitlab::HTTP
response = HTTParty.get(uri.to_s, verify: false) # rubocop:disable Gitlab/HTTParty
return response.body if response.code == 200
raise "HTTP error #{response.code}"
rescue OpenSSL::SSL::SSLError, EOFError => e
raise DevServerSSLError.new(uri, e)
rescue StandardError => e
raise DevServerLoadError.new(uri, e)
end
def self.load_from_static(path)
file_uri = ::Rails.root.join(
Gitlab.config.webpack.output_dir,
path
)
File.read(file_uri)
rescue StandardError => e
raise StaticLoadError.new(file_uri, e)
end
def self.dev_server_path(path)
"/#{Gitlab.config.webpack.public_path}/#{path}"
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Gitlab
module Webpack
class GraphqlKnownOperations
class << self
include Gitlab::Utils::StrongMemoize
def clear_memoization!
clear_memoization(:graphql_known_operations)
end
def load
strong_memoize(:graphql_known_operations) do
data = ::Gitlab::Webpack::FileLoader.load("graphql_known_operations.yml")
YAML.safe_load(data)
rescue StandardError
[]
end
end
end
end
end
end

View File

@ -1,8 +1,5 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'net/http'
require 'uri'
module Gitlab module Gitlab
module Webpack module Webpack
class Manifest class Manifest
@ -78,49 +75,16 @@ module Gitlab
end end
def load_manifest def load_manifest
data = if Gitlab.config.webpack.dev_server.enabled data = Gitlab::Webpack::FileLoader.load(Gitlab.config.webpack.manifest_filename)
load_dev_server_manifest
else
load_static_manifest
end
Gitlab::Json.parse(data) Gitlab::Json.parse(data)
end rescue Gitlab::Webpack::FileLoader::StaticLoadError => e
raise ManifestLoadError.new("Could not load compiled manifest from #{e.uri}.\n\nHave you run `rake gitlab:assets:compile`?", e.original_error)
def load_dev_server_manifest rescue Gitlab::Webpack::FileLoader::DevServerSSLError => e
host = Gitlab.config.webpack.dev_server.host
port = Gitlab.config.webpack.dev_server.port
scheme = Gitlab.config.webpack.dev_server.https ? 'https' : 'http'
uri = Addressable::URI.new(scheme: scheme, host: host, port: port, path: dev_server_path)
# localhost could be blocked via Gitlab::HTTP
response = HTTParty.get(uri.to_s, verify: false) # rubocop:disable Gitlab/HTTParty
return response.body if response.code == 200
raise "HTTP error #{response.code}"
rescue OpenSSL::SSL::SSLError, EOFError => e
ssl_status = Gitlab.config.webpack.dev_server.https ? ' over SSL' : '' ssl_status = Gitlab.config.webpack.dev_server.https ? ' over SSL' : ''
raise ManifestLoadError.new("Could not connect to webpack-dev-server at #{uri}#{ssl_status}.\n\nIs SSL enabled? Check that settings in `gitlab.yml` and webpack-dev-server match.", e) raise ManifestLoadError.new("Could not connect to webpack-dev-server at #{e.uri}#{ssl_status}.\n\nIs SSL enabled? Check that settings in `gitlab.yml` and webpack-dev-server match.", e.original_error)
rescue StandardError => e rescue Gitlab::Webpack::FileLoader::DevServerLoadError => e
raise ManifestLoadError.new("Could not load manifest from webpack-dev-server at #{uri}.\n\nIs webpack-dev-server running? Try running `gdk status webpack` or `gdk tail webpack`.", e) raise ManifestLoadError.new("Could not load manifest from webpack-dev-server at #{e.uri}.\n\nIs webpack-dev-server running? Try running `gdk status webpack` or `gdk tail webpack`.", e.original_error)
end
def load_static_manifest
File.read(static_manifest_path)
rescue StandardError => e
raise ManifestLoadError.new("Could not load compiled manifest from #{static_manifest_path}.\n\nHave you run `rake gitlab:assets:compile`?", e)
end
def static_manifest_path
::Rails.root.join(
Gitlab.config.webpack.output_dir,
Gitlab.config.webpack.manifest_filename
)
end
def dev_server_path
"/#{Gitlab.config.webpack.public_path}/#{Gitlab.config.webpack.manifest_filename}"
end end
end end
end end

View File

@ -100,7 +100,7 @@ module Sidebars
::Sidebars::MenuItem.new( ::Sidebars::MenuItem.new(
title: _('Google Cloud'), title: _('Google Cloud'),
link: project_google_cloud_index_path(context.project), link: project_google_cloud_index_path(context.project),
active_routes: {}, active_routes: { controller: :google_cloud },
item_id: :google_cloud item_id: :google_cloud
) )
end end

View File

@ -36,13 +36,17 @@ namespace :gitlab do
# Do not edit it manually! # Do not edit it manually!
BANNER BANNER
foss_workers, ee_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml foss_workers, ee_workers, jh_workers = Gitlab::SidekiqConfig.workers_for_all_queues_yml
write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, banner, foss_workers) write_yaml(Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH, banner, foss_workers)
if Gitlab.ee? if Gitlab.ee?
write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, banner, ee_workers) write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, banner, ee_workers)
end end
if Gitlab.jh?
write_yaml(Gitlab::SidekiqConfig::JH_QUEUE_CONFIG_PATH, banner, jh_workers)
end
end end
desc 'GitLab | Sidekiq | Validate that all_queues.yml matches worker definitions' desc 'GitLab | Sidekiq | Validate that all_queues.yml matches worker definitions'
@ -57,6 +61,7 @@ namespace :gitlab do
- #{Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH} - #{Gitlab::SidekiqConfig::FOSS_QUEUE_CONFIG_PATH}
- #{Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH} - #{Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH}
#{"- " + Gitlab::SidekiqConfig::JH_QUEUE_CONFIG_PATH if Gitlab.jh?}
MSG MSG
end end

View File

@ -4037,6 +4037,9 @@ msgid_plural "ApplicationSettings|Approve %d users"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "ApplicationSettings|Approve users"
msgstr ""
msgid "ApplicationSettings|Approve users in the pending approval status?" msgid "ApplicationSettings|Approve users in the pending approval status?"
msgstr "" msgstr ""
@ -4045,6 +4048,9 @@ msgid_plural "ApplicationSettings|By making this change, you will automatically
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "ApplicationSettings|By making this change, you will automatically approve all users in pending approval status."
msgstr ""
msgid "ApplicationSettings|Denied domains for sign-ups" msgid "ApplicationSettings|Denied domains for sign-ups"
msgstr "" msgstr ""

View File

@ -227,10 +227,10 @@ GEM
watir (6.19.1) watir (6.19.1)
regexp_parser (>= 1.2, < 3) regexp_parser (>= 1.2, < 3)
selenium-webdriver (>= 3.142.7) selenium-webdriver (>= 3.142.7)
webdrivers (4.7.0) webdrivers (5.0.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
rubyzip (>= 1.3.0) rubyzip (>= 1.3.0)
selenium-webdriver (> 3.141, < 5.0) selenium-webdriver (~> 4.0)
xpath (3.2.0) xpath (3.2.0)
nokogiri (~> 1.8) nokogiri (~> 1.8)
zeitwerk (2.4.2) zeitwerk (2.4.2)
@ -263,10 +263,10 @@ DEPENDENCIES
rspec-retry (~> 0.6.1) rspec-retry (~> 0.6.1)
rspec_junit_formatter (~> 0.4.1) rspec_junit_formatter (~> 0.4.1)
ruby-debug-ide (~> 0.7.0) ruby-debug-ide (~> 0.7.0)
selenium-webdriver (~> 4.0.0.rc1) selenium-webdriver (~> 4.0)
timecop (~> 0.9.1) timecop (~> 0.9.1)
webdrivers (~> 4.6) webdrivers (~> 5.0)
zeitwerk (~> 2.4) zeitwerk (~> 2.4)
BUNDLED WITH BUNDLED WITH
2.2.29 2.2.30

View File

@ -166,6 +166,7 @@ function rspec_paralellized_job() {
export SUITE_FLAKY_RSPEC_REPORT_PATH="${FLAKY_RSPEC_SUITE_REPORT_PATH}" export SUITE_FLAKY_RSPEC_REPORT_PATH="${FLAKY_RSPEC_SUITE_REPORT_PATH}"
export FLAKY_RSPEC_REPORT_PATH="rspec_flaky/all_${report_name}_report.json" export FLAKY_RSPEC_REPORT_PATH="rspec_flaky/all_${report_name}_report.json"
export NEW_FLAKY_RSPEC_REPORT_PATH="rspec_flaky/new_${report_name}_report.json" export NEW_FLAKY_RSPEC_REPORT_PATH="rspec_flaky/new_${report_name}_report.json"
export SKIPPED_FLAKY_TESTS_REPORT_PATH="rspec_flaky/skipped_flaky_tests_${report_name}_report.txt"
if [[ ! -f $FLAKY_RSPEC_REPORT_PATH ]]; then if [[ ! -f $FLAKY_RSPEC_REPORT_PATH ]]; then
echo "{}" > "${FLAKY_RSPEC_REPORT_PATH}" echo "{}" > "${FLAKY_RSPEC_REPORT_PATH}"

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
# We need to distinguish between known and unknown GraphQL operations. This spec
# tests that we set up Gitlab::Graphql::KnownOperations.default which requires
# integration of FE queries, webpack plugin, and BE.
RSpec.describe 'Graphql known operations', :js do
around do |example|
# Let's make sure we aren't receiving or leaving behind any side-effects
# https://gitlab.com/gitlab-org/gitlab/-/jobs/1743294100
::Gitlab::Graphql::KnownOperations.instance_variable_set(:@default, nil)
::Gitlab::Webpack::GraphqlKnownOperations.clear_memoization!
example.run
::Gitlab::Graphql::KnownOperations.instance_variable_set(:@default, nil)
::Gitlab::Webpack::GraphqlKnownOperations.clear_memoization!
end
it 'collects known Graphql operations from the code', :aggregate_failures do
# Check that we include some arbitrary operation name we expect
known_operations = Gitlab::Graphql::KnownOperations.default.operations.map(&:name)
expect(known_operations).to include("searchProjects")
expect(known_operations.length).to be > 20
expect(known_operations).to all( match(%r{^[a-z]+}i) )
end
end

View File

@ -4,25 +4,29 @@ require 'spec_helper'
RSpec.describe 'New/edit issue', :js do RSpec.describe 'New/edit issue', :js do
include ActionView::Helpers::JavaScriptHelper include ActionView::Helpers::JavaScriptHelper
include FormHelper
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user)} let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user)} let_it_be(:user2) { create(:user) }
let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:milestone) { create(:milestone, project: project) }
let_it_be(:label) { create(:label, project: project) } let_it_be(:label) { create(:label, project: project) }
let_it_be(:label2) { create(:label, project: project) } let_it_be(:label2) { create(:label, project: project) }
let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) } let_it_be(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
let(:current_user) { user }
before_all do
project.add_maintainer(user)
project.add_maintainer(user2)
end
before do before do
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false) stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
project.add_maintainer(user) sign_in(current_user)
project.add_maintainer(user2)
sign_in(user)
end end
context 'new issue' do describe 'new issue' do
before do before do
visit new_project_issue_path(project) visit new_project_issue_path(project)
end end
@ -235,29 +239,42 @@ RSpec.describe 'New/edit issue', :js do
end end
describe 'displays issue type options in the dropdown' do describe 'displays issue type options in the dropdown' do
shared_examples 'type option is visible' do |label:, identifier:|
it "shows #{identifier} option", :aggregate_failures do
page.within('[data-testid="issue-type-select-dropdown"]') do
expect(page).to have_selector(%([data-testid="issue-type-#{identifier}-icon"]))
expect(page).to have_content(label)
end
end
end
before do before do
page.within('.issue-form') do page.within('.issue-form') do
click_button 'Issue' click_button 'Issue'
end end
end end
it 'correctly displays the Issue type option with an icon', :aggregate_failures do it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
page.within('[data-testid="issue-type-select-dropdown"]') do it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
expect(page).to have_selector('[data-testid="issue-type-issue-icon"]')
expect(page).to have_content('Issue')
end
end
it 'correctly displays the Incident type option with an icon', :aggregate_failures do context 'when user is guest' do
page.within('[data-testid="issue-type-select-dropdown"]') do let_it_be(:guest) { create(:user) }
expect(page).to have_selector('[data-testid="issue-type-incident-icon"]')
expect(page).to have_content('Incident') let(:current_user) { guest }
before_all do
project.add_guest(guest)
end end
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
end end
end end
describe 'milestone' do describe 'milestone' do
let!(:milestone) { create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project) } let!(:milestone) do
create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project)
end
it 'escapes milestone' do it 'escapes milestone' do
click_button 'Milestone' click_button 'Milestone'
@ -274,7 +291,7 @@ RSpec.describe 'New/edit issue', :js do
end end
end end
context 'edit issue' do describe 'edit issue' do
before do before do
visit edit_project_issue_path(project, issue) visit edit_project_issue_path(project, issue)
end end
@ -329,7 +346,7 @@ RSpec.describe 'New/edit issue', :js do
end end
end end
context 'inline edit' do describe 'inline edit' do
before do before do
visit project_issue_path(project, issue) visit project_issue_path(project, issue)
end end

View File

@ -1,7 +1,7 @@
import { GlBanner } from '@gitlab/ui'; import { GlBanner } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue'; import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
import { INTRO_COOKIE_KEY } from '~/analytics/devops_report/constants'; import { INTRO_COOKIE_KEY } from '~/analytics/devops_reports/constants';
import * as utils from '~/lib/utils/common_utils'; import * as utils from '~/lib/utils/common_utils';
import { devopsReportDocsPath, devopsScoreIntroImagePath } from '../mock_data'; import { devopsReportDocsPath, devopsScoreIntroImagePath } from '../mock_data';

View File

@ -2,8 +2,8 @@ import { GlTable, GlBadge, GlEmptyState } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts'; import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue'; import DevopsScore from '~/analytics/devops_reports/components/devops_score.vue';
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue'; import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
import { devopsScoreMetricsData, noDataImagePath, devopsScoreTableHeaders } from '../mock_data'; import { devopsScoreMetricsData, noDataImagePath, devopsScoreTableHeaders } from '../mock_data';
describe('DevopsScore', () => { describe('DevopsScore', () => {

View File

@ -1,9 +1,9 @@
import { GlEmptyState, GlSprintf } from '@gitlab/ui'; import { GlEmptyState, GlSprintf } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import ServicePingDisabled from '~/analytics/devops_report/components/service_ping_disabled.vue'; import ServicePingDisabled from '~/analytics/devops_reports/components/service_ping_disabled.vue';
describe('~/analytics/devops_report/components/service_ping_disabled.vue', () => { describe('~/analytics/devops_reports/components/service_ping_disabled.vue', () => {
let wrapper; let wrapper;
afterEach(() => { afterEach(() => {

View File

@ -1,6 +1,7 @@
import { GlIcon } from '@gitlab/ui'; import { GlIcon } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils'; import { mount, createLocalVue } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue'; import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import { createStore } from '~/mr_notes/stores'; import { createStore } from '~/mr_notes/stores';
import DiscussionNotes from '~/notes/components/discussion_notes.vue'; import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue'; import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
@ -19,6 +20,9 @@ describe('DiffDiscussions', () => {
store = createStore(); store = createStore();
wrapper = mount(localVue.extend(DiffDiscussions), { wrapper = mount(localVue.extend(DiffDiscussions), {
store, store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: { propsData: {
discussions: getDiscussionsMockData(), discussions: getDiscussionsMockData(),
...props, ...props,

View File

@ -0,0 +1,133 @@
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
describe('Diff Discussions Utils', () => {
describe('discussionIntersectionObserverHandlerFactory', () => {
it('creates a handler function', () => {
expect(discussionIntersectionObserverHandlerFactory()).toBeInstanceOf(Function);
});
describe('intersection observer handler', () => {
const functions = {
setCurrentDiscussionId: jest.fn(),
getPreviousUnresolvedDiscussionId: jest.fn().mockImplementation((id) => {
return Number(id) - 1;
}),
};
const defaultProcessableWrapper = {
entry: {
time: 0,
isIntersecting: true,
rootBounds: {
bottom: 0,
},
boundingClientRect: {
top: 0,
},
},
currentDiscussion: {
id: 1,
},
isFirstUnresolved: false,
isDiffsPage: true,
};
let handler;
let getMock;
let setMock;
beforeEach(() => {
functions.setCurrentDiscussionId.mockClear();
functions.getPreviousUnresolvedDiscussionId.mockClear();
defaultProcessableWrapper.functions = functions;
setMock = functions.setCurrentDiscussionId.mock;
getMock = functions.getPreviousUnresolvedDiscussionId.mock;
handler = discussionIntersectionObserverHandlerFactory();
});
it('debounces multiple simultaneous requests into one queue', () => {
handler(defaultProcessableWrapper);
handler(defaultProcessableWrapper);
handler(defaultProcessableWrapper);
handler(defaultProcessableWrapper);
expect(setTimeout).toHaveBeenCalledTimes(4);
expect(clearTimeout).toHaveBeenCalledTimes(3);
// By only advancing to one timer, we ensure it's all being batched into one queue
jest.advanceTimersToNextTimer();
expect(functions.setCurrentDiscussionId).toHaveBeenCalledTimes(4);
});
it('properly processes, sorts and executes the correct actions for a set of observed intersections', () => {
handler(defaultProcessableWrapper);
handler({
// This observation is here to be filtered out because it's a scrollDown
...defaultProcessableWrapper,
entry: {
...defaultProcessableWrapper.entry,
isIntersecting: false,
boundingClientRect: { top: 10 },
rootBounds: { bottom: 100 },
},
});
handler({
...defaultProcessableWrapper,
entry: {
...defaultProcessableWrapper.entry,
time: 101,
isIntersecting: false,
rootBounds: { bottom: -100 },
},
currentDiscussion: { id: 20 },
});
handler({
...defaultProcessableWrapper,
entry: {
...defaultProcessableWrapper.entry,
time: 100,
isIntersecting: false,
boundingClientRect: { top: 100 },
},
currentDiscussion: { id: 30 },
isDiffsPage: false,
});
handler({
...defaultProcessableWrapper,
isFirstUnresolved: true,
entry: {
...defaultProcessableWrapper.entry,
time: 100,
isIntersecting: false,
boundingClientRect: { top: 200 },
},
});
jest.advanceTimersToNextTimer();
expect(setMock.calls.length).toBe(4);
expect(setMock.calls[0]).toEqual([1]);
expect(setMock.calls[1]).toEqual([29]);
expect(setMock.calls[2]).toEqual([null]);
expect(setMock.calls[3]).toEqual([19]);
expect(getMock.calls.length).toBe(2);
expect(getMock.calls[0]).toEqual([30, false]);
expect(getMock.calls[1]).toEqual([20, true]);
[
setMock.invocationCallOrder[0],
getMock.invocationCallOrder[0],
setMock.invocationCallOrder[1],
setMock.invocationCallOrder[2],
getMock.invocationCallOrder[1],
setMock.invocationCallOrder[3],
].forEach((order, idx, list) => {
// Compare each invocation sequence to the one before it (except the first one)
expect(list[idx - 1] || -1).toBeLessThan(order);
});
});
});
});
});

View File

@ -1,6 +1,7 @@
import { getByRole } from '@testing-library/dom'; import { getByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import '~/behaviors/markdown/render_gfm'; import '~/behaviors/markdown/render_gfm';
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import DiscussionNotes from '~/notes/components/discussion_notes.vue'; import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import NoteableNote from '~/notes/components/noteable_note.vue'; import NoteableNote from '~/notes/components/noteable_note.vue';
import { SYSTEM_NOTE } from '~/notes/constants'; import { SYSTEM_NOTE } from '~/notes/constants';
@ -26,6 +27,9 @@ describe('DiscussionNotes', () => {
const createComponent = (props, mountingMethod = shallowMount) => { const createComponent = (props, mountingMethod = shallowMount) => {
wrapper = mountingMethod(DiscussionNotes, { wrapper = mountingMethod(DiscussionNotes, {
store, store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: { propsData: {
discussion: discussionMock, discussion: discussionMock,
isExpanded: false, isExpanded: false,

View File

@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json'; import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
import { trimText } from 'helpers/text_helper'; import { trimText } from 'helpers/text_helper';
import mockDiffFile from 'jest/diffs/mock_data/diff_file'; import mockDiffFile from 'jest/diffs/mock_data/diff_file';
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import DiscussionNotes from '~/notes/components/discussion_notes.vue'; import DiscussionNotes from '~/notes/components/discussion_notes.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue'; import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue'; import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
@ -31,6 +32,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, { wrapper = mount(NoteableDiscussion, {
store, store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: { discussion: discussionMock }, propsData: { discussion: discussionMock },
}); });
}); });
@ -167,6 +171,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, { wrapper = mount(NoteableDiscussion, {
store, store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: { discussion: discussionMock }, propsData: { discussion: discussionMock },
}); });
}); });
@ -185,6 +192,9 @@ describe('noteable_discussion component', () => {
wrapper = mount(NoteableDiscussion, { wrapper = mount(NoteableDiscussion, {
store, store,
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData: { discussion: discussionMock }, propsData: { discussion: discussionMock },
}); });
}); });

View File

@ -9,6 +9,7 @@ import DraftNote from '~/batch_comments/components/draft_note.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments'; import batchComments from '~/batch_comments/stores/modules/batch_comments';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility'; import * as urlUtility from '~/lib/utils/url_utility';
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
import CommentForm from '~/notes/components/comment_form.vue'; import CommentForm from '~/notes/components/comment_form.vue';
import NotesApp from '~/notes/components/notes_app.vue'; import NotesApp from '~/notes/components/notes_app.vue';
import * as constants from '~/notes/constants'; import * as constants from '~/notes/constants';
@ -78,6 +79,9 @@ describe('note_app', () => {
</div>`, </div>`,
}, },
{ {
provide: {
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
},
propsData, propsData,
store, store,
}, },

View File

@ -0,0 +1,72 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'rspec-parameterized'
require "support/graphql/fake_query_type"
RSpec.describe Gitlab::Graphql::KnownOperations do
using RSpec::Parameterized::TableSyntax
# Include duplicated operation names to test that we are unique-ifying them
let(:fake_operations) { %w(foo foo bar bar) }
let(:fake_schema) do
Class.new(GraphQL::Schema) do
query Graphql::FakeQueryType
end
end
subject { described_class.new(fake_operations) }
describe "#from_query" do
where(:query_string, :expected) do
"query { helloWorld }" | described_class::ANONYMOUS
"query fuzzyyy { helloWorld }" | described_class::UNKNOWN
"query foo { helloWorld }" | described_class::Operation.new("foo")
end
with_them do
it "returns known operation name from GraphQL Query" do
query = ::GraphQL::Query.new(fake_schema, query_string)
expect(subject.from_query(query)).to eq(expected)
end
end
end
describe "#operations" do
it "returns array of known operations" do
expect(subject.operations.map(&:name)).to match_array(%w(anonymous unknown foo bar))
end
end
describe "Operation#to_caller_id" do
where(:query_string, :expected) do
"query { helloWorld }" | "graphql:#{described_class::ANONYMOUS.name}"
"query foo { helloWorld }" | "graphql:foo"
end
with_them do
it "formats operation name for caller_id metric property" do
query = ::GraphQL::Query.new(fake_schema, query_string)
expect(subject.from_query(query).to_caller_id).to eq(expected)
end
end
end
describe ".default" do
it "returns a memoization of values from webpack", :aggregate_failures do
# .default could have been referenced in another spec, so we need to clean it up here
described_class.instance_variable_set(:@default, nil)
expect(Gitlab::Webpack::GraphqlKnownOperations).to receive(:load).once.and_return(fake_operations)
2.times { described_class.default }
# Uses reference equality to verify memoization
expect(described_class.default).to equal(described_class.default)
expect(described_class.default).to be_a(described_class)
expect(described_class.default.operations.map(&:name)).to include(*fake_operations)
end
end
end

View File

@ -11,12 +11,12 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
end end
def stub_exists(exists: true) def stub_exists(exists: true)
['app/workers/all_queues.yml', 'ee/app/workers/all_queues.yml'].each do |path| ['app/workers/all_queues.yml', 'ee/app/workers/all_queues.yml', 'jh/app/workers/all_queues.yml'].each do |path|
allow(File).to receive(:exist?).with(expand_path(path)).and_return(exists) allow(File).to receive(:exist?).with(expand_path(path)).and_return(exists)
end end
end end
def stub_contents(foss_queues, ee_queues) def stub_contents(foss_queues, ee_queues, jh_queues)
allow(YAML).to receive(:load_file) allow(YAML).to receive(:load_file)
.with(expand_path('app/workers/all_queues.yml')) .with(expand_path('app/workers/all_queues.yml'))
.and_return(foss_queues) .and_return(foss_queues)
@ -24,6 +24,10 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
allow(YAML).to receive(:load_file) allow(YAML).to receive(:load_file)
.with(expand_path('ee/app/workers/all_queues.yml')) .with(expand_path('ee/app/workers/all_queues.yml'))
.and_return(ee_queues) .and_return(ee_queues)
allow(YAML).to receive(:load_file)
.with(expand_path('jh/app/workers/all_queues.yml'))
.and_return(jh_queues)
end end
before do before do
@ -45,8 +49,9 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
end end
it 'flattens and joins the contents' do it 'flattens and joins the contents' do
expected_queues = %w[queue_a queue_b] expected_queues = %w[queue_a]
expected_queues = expected_queues.first(1) unless Gitlab.ee? expected_queues << 'queue_b' if Gitlab.ee?
expected_queues << 'queue_c' if Gitlab.jh?
expect(described_class.worker_queues(dummy_root)) expect(described_class.worker_queues(dummy_root))
.to match_array(expected_queues) .to match_array(expected_queues)
@ -55,7 +60,7 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
context 'when the file contains an array of hashes' do context 'when the file contains an array of hashes' do
before do before do
stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }]) stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }], [{ name: 'queue_c' }])
end end
include_examples 'valid file contents' include_examples 'valid file contents'

View File

@ -18,19 +18,26 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
get_tags: attributes[:tags] get_tags: attributes[:tags]
) )
described_class.new(inner_worker, ee: false) described_class.new(inner_worker, ee: false, jh: false)
end end
describe '#ee?' do describe '#ee?' do
it 'returns the EE status set on creation' do it 'returns the EE status set on creation' do
expect(described_class.new(double, ee: true)).to be_ee expect(described_class.new(double, ee: true, jh: false)).to be_ee
expect(described_class.new(double, ee: false)).not_to be_ee expect(described_class.new(double, ee: false, jh: false)).not_to be_ee
end
end
describe '#jh?' do
it 'returns the JH status set on creation' do
expect(described_class.new(double, ee: false, jh: true)).to be_jh
expect(described_class.new(double, ee: false, jh: false)).not_to be_jh
end end
end end
describe '#==' do describe '#==' do
def worker_with_yaml(yaml) def worker_with_yaml(yaml)
described_class.new(double, ee: false).tap do |worker| described_class.new(double, ee: false, jh: false).tap do |worker|
allow(worker).to receive(:to_yaml).and_return(yaml) allow(worker).to receive(:to_yaml).and_return(yaml)
end end
end end
@ -57,7 +64,7 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
expect(worker).to receive(meth) expect(worker).to receive(meth)
described_class.new(worker, ee: false).send(meth) described_class.new(worker, ee: false, jh: false).send(meth)
end end
end end
end end

View File

@ -0,0 +1,79 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'support/helpers/file_read_helpers'
require 'support/webmock'
RSpec.describe Gitlab::Webpack::FileLoader do
include FileReadHelpers
include WebMock::API
let(:error_file_path) { "error.yml" }
let(:file_path) { "my_test_file.yml" }
let(:file_contents) do
<<-EOF
- hello
- world
- test
EOF
end
before do
allow(Gitlab.config.webpack.dev_server).to receive_messages(host: 'hostname', port: 2000, https: false)
allow(Gitlab.config.webpack).to receive(:public_path).and_return('public_path')
allow(Gitlab.config.webpack).to receive(:output_dir).and_return('webpack_output')
end
context "with dev server enabled" do
before do
allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true)
stub_request(:get, "http://hostname:2000/public_path/not_found").to_return(status: 404)
stub_request(:get, "http://hostname:2000/public_path/#{file_path}").to_return(body: file_contents, status: 200)
stub_request(:get, "http://hostname:2000/public_path/#{error_file_path}").to_raise(StandardError)
end
it "returns content when respondes succesfully" do
expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
end
it "raises error when 404" do
expect { Gitlab::Webpack::FileLoader.load("not_found") }.to raise_error("HTTP error 404")
end
it "raises error when errors out" do
expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerLoadError)
end
end
context "with dev server enabled and https" do
before do
allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(true)
allow(Gitlab.config.webpack.dev_server).to receive(:https).and_return(true)
stub_request(:get, "https://hostname:2000/public_path/#{error_file_path}").to_raise(EOFError)
end
it "raises error if catches SSLError" do
expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::DevServerSSLError)
end
end
context "with dev server disabled" do
before do
allow(Gitlab.config.webpack.dev_server).to receive(:enabled).and_return(false)
stub_file_read(::Rails.root.join("webpack_output/#{file_path}"), content: file_contents)
stub_file_read(::Rails.root.join("webpack_output/#{error_file_path}"), error: Errno::ENOENT)
end
describe ".load" do
it "returns file content from file path" do
expect(Gitlab::Webpack::FileLoader.load(file_path)).to be(file_contents)
end
it "throws error if file cannot be read" do
expect { Gitlab::Webpack::FileLoader.load(error_file_path) }.to raise_error(Gitlab::Webpack::FileLoader::StaticLoadError)
end
end
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::Webpack::GraphqlKnownOperations do
let(:content) do
<<-EOF
- hello
- world
- test
EOF
end
around do |example|
described_class.clear_memoization!
example.run
described_class.clear_memoization!
end
describe ".load" do
context "when file loader returns" do
before do
allow(::Gitlab::Webpack::FileLoader).to receive(:load).with("graphql_known_operations.yml").and_return(content)
end
it "returns memoized value" do
expect(::Gitlab::Webpack::FileLoader).to receive(:load).once
2.times { ::Gitlab::Webpack::GraphqlKnownOperations.load }
expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq(%w(hello world test))
end
end
context "when file loader errors" do
before do
allow(::Gitlab::Webpack::FileLoader).to receive(:load).and_raise(StandardError.new("test"))
end
it "returns empty array" do
expect(::Gitlab::Webpack::GraphqlKnownOperations.load).to eq([])
end
end
end
end

View File

@ -51,6 +51,16 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
it 'menu link points to Terraform page' do it 'menu link points to Terraform page' do
expect(subject.link).to eq find_menu_item(:terraform).link expect(subject.link).to eq find_menu_item(:terraform).link
end end
context 'when Terraform menu is not visible' do
before do
subject.renderable_items.delete(find_menu_item(:terraform))
end
it 'menu link points to Google Cloud page' do
expect(subject.link).to eq find_menu_item(:google_cloud).link
end
end
end end
end end
@ -89,5 +99,11 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
it_behaves_like 'access rights checks' it_behaves_like 'access rights checks'
end end
describe 'Google Cloud' do
let(:item_id) { :google_cloud }
it_behaves_like 'access rights checks'
end
end end
end end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AuthorizedProjectUpdate::ProjectAccessChangedService do
describe '#execute' do
it 'schedules the project IDs' do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_and_wait)
.with([[1], [2]])
described_class.new([1, 2]).execute
end
it 'permits non-blocking operation' do
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
.with([[1], [2]])
described_class.new([1, 2]).execute(blocking: false)
end
end
end

View File

@ -593,11 +593,16 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
let_it_be_with_reload(:group) { create(:group, :private, parent: old_parent_group) } let_it_be_with_reload(:group) { create(:group, :private, parent: old_parent_group) }
let_it_be(:new_group_member) { create(:user) } let_it_be(:new_group_member) { create(:user) }
let_it_be(:old_group_member) { create(:user) } let_it_be(:old_group_member) { create(:user) }
let_it_be(:unique_subgroup_member) { create(:user) }
let_it_be(:direct_project_member) { create(:user) }
before do before do
new_parent_group.add_maintainer(new_group_member) new_parent_group.add_maintainer(new_group_member)
old_parent_group.add_maintainer(old_group_member) old_parent_group.add_maintainer(old_group_member)
subgroup1.add_developer(unique_subgroup_member)
nested_project.add_developer(direct_project_member)
group.refresh_members_authorized_projects group.refresh_members_authorized_projects
subgroup1.refresh_members_authorized_projects
end end
it 'removes old project authorizations' do it 'removes old project authorizations' do
@ -613,7 +618,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end end
it 'performs authorizations job immediately' do it 'performs authorizations job immediately' do
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_inline) expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_inline)
transfer_service.execute(new_parent_group) transfer_service.execute(new_parent_group)
end end
@ -630,14 +635,24 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
ProjectAuthorization.where(project_id: nested_project.id, user_id: new_group_member.id).size ProjectAuthorization.where(project_id: nested_project.id, user_id: new_group_member.id).size
}.from(0).to(1) }.from(0).to(1)
end end
it 'preserves existing project authorizations for direct project members' do
expect { transfer_service.execute(new_parent_group) }.not_to change {
ProjectAuthorization.where(project_id: nested_project.id, user_id: direct_project_member.id).count
}
end
end end
context 'for groups with many members' do context 'for nested groups with unique members' do
before do it 'preserves existing project authorizations' do
11.times do expect { transfer_service.execute(new_parent_group) }.not_to change {
new_parent_group.add_maintainer(create(:user)) ProjectAuthorization.where(project_id: nested_project.id, user_id: unique_subgroup_member.id).count
end }
end end
end
context 'for groups with many projects' do
let_it_be(:project_list) { create_list(:project, 11, :repository, :private, namespace: group) }
it 'adds new project authorizations for the user which makes a transfer' do it 'adds new project authorizations for the user which makes a transfer' do
transfer_service.execute(new_parent_group) transfer_service.execute(new_parent_group)
@ -646,9 +661,21 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
expect(ProjectAuthorization.where(project_id: nested_project.id, user_id: user.id).size).to eq(1) expect(ProjectAuthorization.where(project_id: nested_project.id, user_id: user.id).size).to eq(1)
end end
it 'adds project authorizations for users in the new hierarchy' do
expect { transfer_service.execute(new_parent_group) }.to change {
ProjectAuthorization.where(project_id: project_list.map { |project| project.id }, user_id: new_group_member.id).size
}.from(0).to(project_list.count)
end
it 'removes project authorizations for users in the old hierarchy' do
expect { transfer_service.execute(new_parent_group) }.to change {
ProjectAuthorization.where(project_id: project_list.map { |project| project.id }, user_id: old_group_member.id).size
}.from(project_list.count).to(0)
end
it 'schedules authorizations job' do it 'schedules authorizations job' do
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async) expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
.with(array_including(new_parent_group.members_with_parents.pluck(:user_id).map {|id| [id, anything] })) .with(array_including(group.all_projects.ids.map { |id| [id, anything] }))
transfer_service.execute(new_parent_group) transfer_service.execute(new_parent_group)
end end

View File

@ -107,9 +107,7 @@ RSpec.configure do |config|
warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2` warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2`
end end
end end
end else
unless ENV['CI']
# Allow running `:focus` examples locally, # Allow running `:focus` examples locally,
# falling back to all tests when there is no `:focus` example. # falling back to all tests when there is no `:focus` example.
config.filter_run focus: true config.filter_run focus: true

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
return unless ENV['CI']
return unless ENV['SKIP_FLAKY_TESTS_AUTOMATICALLY'] == "true"
return if ENV['CI_MERGE_REQUEST_LABELS'].include?(/pipeline:run-flaky-tests/)
require_relative '../tooling/rspec_flaky/report'
RSpec.configure do |config|
$flaky_test_example_ids = begin # rubocop:disable Style/GlobalVars
raise "$SUITE_FLAKY_RSPEC_REPORT_PATH is empty." if ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'].to_s.empty?
raise "#{ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']} doesn't exist" unless File.exist?(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH'])
RspecFlaky::Report.load(ENV['SUITE_FLAKY_RSPEC_REPORT_PATH']).map { |_, flaky_test_data| flaky_test_data["example_id"] }
rescue => e # rubocop:disable Style/RescueStandardError
puts e
[]
end
$skipped_flaky_tests_report = [] # rubocop:disable Style/GlobalVars
config.around do |example|
# Skip flaky tests automatically
if $flaky_test_example_ids.include?(example.id) # rubocop:disable Style/GlobalVars
puts "Skipping #{example.id} '#{example.full_description}' because it's flaky."
$skipped_flaky_tests_report << example.id # rubocop:disable Style/GlobalVars
else
example.run
end
end
config.after(:suite) do
next unless ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH']
File.write(ENV['SKIPPED_FLAKY_TESTS_REPORT_PATH'], "#{$skipped_flaky_tests_report.join("\n")}\n") # rubocop:disable Style/GlobalVars
end
end

View File

@ -82,8 +82,9 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
nil | 10 | nil nil | 10 | nil
0 | 5 | nil 0 | 5 | nil
10 | 0 | 0 10 | 0 | 0
10 | 5 | 0.5 10 | 5 | 50.0
3 | 10 | (10 / 3.to_f) 17 | 3 | 17.65
3 | 10 | 333.33
end end
with_them do with_them do