Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6a38034714
commit
05db4ead6d
79 changed files with 1189 additions and 312 deletions
|
@ -534,6 +534,50 @@ rspec:feature-flags:
|
|||
run_timed_command "bundle exec scripts/used-feature-flags";
|
||||
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 #
|
||||
#######################################################
|
||||
|
||||
|
|
|
@ -1352,6 +1352,13 @@
|
|||
when: never
|
||||
- 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 #
|
||||
#########################
|
||||
|
|
|
@ -1 +1 @@
|
|||
7b9cd199b0851fd1b6615e0798f2aafddafd63cb
|
||||
460a880c6993ab5f76cac951fccc02efd5cbd444
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -342,7 +342,7 @@ group :development do
|
|||
gem 'lefthook', '~> 0.7.0', 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
|
||||
gem 'better_errors', '~> 2.9.0'
|
||||
|
|
11
Gemfile.lock
11
Gemfile.lock
|
@ -700,10 +700,11 @@ GEM
|
|||
lefthook (0.7.5)
|
||||
letter_opener (1.7.0)
|
||||
launchy (~> 2.2)
|
||||
letter_opener_web (1.4.1)
|
||||
actionmailer (>= 3.2)
|
||||
letter_opener (~> 1.0)
|
||||
railties (>= 3.2)
|
||||
letter_opener_web (2.0.0)
|
||||
actionmailer (>= 5.2)
|
||||
letter_opener (~> 1.7)
|
||||
railties (>= 5.2)
|
||||
rexml
|
||||
libyajl2 (1.2.0)
|
||||
license_finder (6.0.0)
|
||||
bundler
|
||||
|
@ -1516,7 +1517,7 @@ DEPENDENCIES
|
|||
kramdown (~> 2.3.1)
|
||||
kubeclient (~> 4.9.2)
|
||||
lefthook (~> 0.7.0)
|
||||
letter_opener_web (~> 1.4.1)
|
||||
letter_opener_web (~> 2.0.0)
|
||||
license_finder (~> 6.0)
|
||||
licensee (~> 9.14.1)
|
||||
lockbox (~> 0.6.2)
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
query GroupBoardIterations($fullPath: ID!, $title: String) {
|
||||
group(fullPath: $fullPath) {
|
||||
iterations(includeAncestors: true, title: $title) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
query ProjectBoardIterations($fullPath: ID!, $title: String) {
|
||||
project(fullPath: $fullPath) {
|
||||
iterations(includeAncestors: true, title: $title) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,13 +36,11 @@ import {
|
|||
} from '../boards_util';
|
||||
import { gqlClient } from '../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 groupProjectsQuery from '../graphql/group_projects.query.graphql';
|
||||
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
|
||||
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.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 * 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) {
|
||||
commit(types.RECEIVE_MILESTONES_REQUEST);
|
||||
|
||||
|
|
|
@ -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 RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
|
||||
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';
|
||||
|
|
|
@ -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 }) {
|
||||
state.activeId = id;
|
||||
state.sidebarType = sidebarType;
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
TRACKING_MULTIPLE_FILES_MODE,
|
||||
} from '../constants';
|
||||
|
||||
import { discussionIntersectionObserverHandlerFactory } from '../utils/discussions';
|
||||
import diffsEventHub from '../event_hub';
|
||||
import { reviewStatuses } from '../utils/file_reviews';
|
||||
import { diffsApp } from '../utils/performance';
|
||||
|
@ -86,6 +87,9 @@ export default {
|
|||
ALERT_MERGE_CONFLICT,
|
||||
ALERT_COLLAPSED_FILES,
|
||||
},
|
||||
provide: {
|
||||
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||
},
|
||||
props: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
|
|
76
app/assets/javascripts/diffs/utils/discussions.js
Normal file
76
app/assets/javascripts/diffs/utils/discussions.js
Normal 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);
|
||||
};
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { GlIntersectionObserver } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
|
||||
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
|
||||
|
@ -16,7 +17,9 @@ export default {
|
|||
ToggleRepliesWidget,
|
||||
NoteEditedText,
|
||||
DiscussionNotesRepliesWrapper,
|
||||
GlIntersectionObserver,
|
||||
},
|
||||
inject: ['discussionObserverHandler'],
|
||||
props: {
|
||||
discussion: {
|
||||
type: Object,
|
||||
|
@ -54,7 +57,11 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userCanReply']),
|
||||
...mapGetters([
|
||||
'userCanReply',
|
||||
'previousUnresolvedDiscussionId',
|
||||
'firstUnresolvedDiscussionId',
|
||||
]),
|
||||
hasReplies() {
|
||||
return Boolean(this.replies.length);
|
||||
},
|
||||
|
@ -77,9 +84,20 @@ export default {
|
|||
url: this.discussion.discussion_path,
|
||||
};
|
||||
},
|
||||
isFirstUnresolved() {
|
||||
return this.firstUnresolvedDiscussionId === this.discussion.id;
|
||||
},
|
||||
},
|
||||
observerOptions: {
|
||||
threshold: 0,
|
||||
rootMargin: '0px 0px -50% 0px',
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['toggleDiscussion', 'setSelectedCommentPositionHover']),
|
||||
...mapActions([
|
||||
'toggleDiscussion',
|
||||
'setSelectedCommentPositionHover',
|
||||
'setCurrentDiscussionId',
|
||||
]),
|
||||
componentName(note) {
|
||||
if (note.isPlaceholderNote) {
|
||||
if (note.placeholderType === SYSTEM_NOTE) {
|
||||
|
@ -110,6 +128,18 @@ export default {
|
|||
this.setSelectedCommentPositionHover();
|
||||
}
|
||||
},
|
||||
observerTriggered(entry) {
|
||||
this.discussionObserverHandler({
|
||||
entry,
|
||||
isFirstUnresolved: this.isFirstUnresolved,
|
||||
currentDiscussion: { ...this.discussion },
|
||||
isDiffsPage: !this.isOverviewTab,
|
||||
functions: {
|
||||
setCurrentDiscussionId: this.setCurrentDiscussionId,
|
||||
getPreviousUnresolvedDiscussionId: this.previousUnresolvedDiscussionId,
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -122,33 +152,35 @@ export default {
|
|||
@mouseleave="handleMouseLeave(discussion)"
|
||||
>
|
||||
<template v-if="shouldGroupReplies">
|
||||
<component
|
||||
:is="componentName(firstNote)"
|
||||
:note="componentData(firstNote)"
|
||||
:line="line || diffLine"
|
||||
:discussion-file="discussion.diff_file"
|
||||
:commit="commit"
|
||||
:help-page-path="helpPagePath"
|
||||
:show-reply-button="userCanReply"
|
||||
:discussion-root="true"
|
||||
:discussion-resolve-path="discussion.resolve_path"
|
||||
:is-overview-tab="isOverviewTab"
|
||||
@handleDeleteNote="$emit('deleteNote')"
|
||||
@startReplying="$emit('startReplying')"
|
||||
>
|
||||
<template #discussion-resolved-text>
|
||||
<note-edited-text
|
||||
v-if="discussion.resolved"
|
||||
:edited-at="discussion.resolved_at"
|
||||
:edited-by="discussion.resolved_by"
|
||||
:action-text="resolvedText"
|
||||
class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
|
||||
/>
|
||||
</template>
|
||||
<template #avatar-badge>
|
||||
<slot name="avatar-badge"></slot>
|
||||
</template>
|
||||
</component>
|
||||
<gl-intersection-observer :options="$options.observerOptions" @update="observerTriggered">
|
||||
<component
|
||||
:is="componentName(firstNote)"
|
||||
:note="componentData(firstNote)"
|
||||
:line="line || diffLine"
|
||||
:discussion-file="discussion.diff_file"
|
||||
:commit="commit"
|
||||
:help-page-path="helpPagePath"
|
||||
:show-reply-button="userCanReply"
|
||||
:discussion-root="true"
|
||||
:discussion-resolve-path="discussion.resolve_path"
|
||||
:is-overview-tab="isOverviewTab"
|
||||
@handleDeleteNote="$emit('deleteNote')"
|
||||
@startReplying="$emit('startReplying')"
|
||||
>
|
||||
<template #discussion-resolved-text>
|
||||
<note-edited-text
|
||||
v-if="discussion.resolved"
|
||||
:edited-at="discussion.resolved_at"
|
||||
:edited-by="discussion.resolved_by"
|
||||
:action-text="resolvedText"
|
||||
class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
|
||||
/>
|
||||
</template>
|
||||
<template #avatar-badge>
|
||||
<slot name="avatar-badge"></slot>
|
||||
</template>
|
||||
</component>
|
||||
</gl-intersection-observer>
|
||||
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
|
||||
<toggle-replies-widget
|
||||
v-if="hasReplies"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import initDevOpsScore from '~/analytics/devops_report/devops_score';
|
||||
import initDevOpsScoreDisabledServicePing from '~/analytics/devops_report/devops_score_disabled_service_ping';
|
||||
import initDevOpsScore from '~/analytics/devops_reports/devops_score';
|
||||
import initDevOpsScoreDisabledServicePing from '~/analytics/devops_reports/devops_score_disabled_service_ping';
|
||||
|
||||
initDevOpsScoreDisabledServicePing();
|
||||
initDevOpsScore();
|
||||
|
|
|
@ -227,7 +227,7 @@
|
|||
// IMPORTANT PERFORMANCE OPTIMIZATION
|
||||
//
|
||||
// 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 {
|
||||
content-visibility: auto;
|
||||
contain-intrinsic-size: 1px 3em;
|
||||
|
@ -237,6 +237,10 @@
|
|||
content-visibility: auto;
|
||||
contain-intrinsic-size: 1px 1.1875rem;
|
||||
}
|
||||
|
||||
.line-numbers {
|
||||
content-visibility: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.logs {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module IssuesHelper
|
||||
include Issues::IssueTypeHelpers
|
||||
|
||||
def issue_css_classes(issue)
|
||||
classes = ["issue"]
|
||||
classes << "closed" if issue.closed?
|
||||
|
|
|
@ -41,8 +41,6 @@ module AlertEventLifecycle
|
|||
scope :firing, -> { where(status: status_value_for(:firing)) }
|
||||
scope :resolved, -> { where(status: status_value_for(:resolved)) }
|
||||
|
||||
scope :count_by_project_id, -> { group(:project_id).count }
|
||||
|
||||
def self.status_value_for(name)
|
||||
state_machines[:status].states[name].value
|
||||
end
|
||||
|
|
|
@ -92,7 +92,6 @@ module Issuable
|
|||
scope :recent, -> { reorder(id: :desc) }
|
||||
scope :of_projects, ->(ids) { where(project_id: ids) }
|
||||
scope :opened, -> { with_state(:opened) }
|
||||
scope :only_opened, -> { with_state(:opened) }
|
||||
scope :closed, -> { with_state(:closed) }
|
||||
|
||||
# rubocop:disable GitlabSecurity/SqlInjection
|
||||
|
|
|
@ -14,7 +14,6 @@ module Milestoneable
|
|||
|
||||
validate :milestone_is_valid
|
||||
|
||||
scope :of_milestones, ->(ids) { where(milestone_id: ids) }
|
||||
scope :any_milestone, -> { where.not(milestone_id: nil) }
|
||||
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) }
|
||||
|
|
|
@ -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
|
12
app/services/concerns/issues/issue_type_helpers.rb
Normal file
12
app/services/concerns/issues/issue_type_helpers.rb
Normal 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
|
|
@ -175,21 +175,18 @@ module Groups
|
|||
end
|
||||
|
||||
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
|
||||
current_user.refresh_authorized_projects
|
||||
|
||||
# schedule refreshing projects for all the members of the group
|
||||
@group.refresh_members_authorized_projects
|
||||
# All projects in this hierarchy need to have their project authorizations recalculated
|
||||
@group.all_projects.each_batch { |prjs| projects_to_update.merge(prjs.ids) } # rubocop: disable CodeReuse/ActiveRecord
|
||||
|
||||
# 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.
|
||||
project_group_shares_within_the_hierarchy = ProjectGroupLink.in_group(group.self_and_descendants.select(:id))
|
||||
|
||||
project_group_shares_within_the_hierarchy.find_each do |project_group_link|
|
||||
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project_group_link.project_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
|
||||
end
|
||||
|
||||
AuthorizedProjectUpdate::ProjectAccessChangedService.new(projects_to_update.to_a).execute unless projects_to_update.empty?
|
||||
end
|
||||
|
||||
def raise_transfer_error(message)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module Issues
|
||||
class BaseService < ::IssuableBaseService
|
||||
include IncidentManagement::UsageData
|
||||
include IssueTypeHelpers
|
||||
|
||||
def hook_data(issue, action, old_associations: {})
|
||||
hook_data = issue.to_hook_data(current_user, old_associations: old_associations)
|
||||
|
@ -44,7 +45,7 @@ module Issues
|
|||
def filter_params(issue)
|
||||
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]
|
||||
|
||||
moved_issue = params.delete(:moved_issue)
|
||||
|
@ -89,12 +90,6 @@ module Issues
|
|||
Milestones::IssuesCountService.new(milestone).delete_cache
|
||||
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]
|
||||
def filter_incident_label(issue)
|
||||
return unless add_incident_label?(issue) || remove_incident_label?(issue)
|
||||
|
|
|
@ -80,7 +80,7 @@ module Issues
|
|||
]
|
||||
|
||||
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)
|
||||
end
|
||||
|
|
|
@ -6,5 +6,5 @@
|
|||
- if show_adoption?
|
||||
= render_if_exists 'admin/dev_ops_report/devops_tabs'
|
||||
- else
|
||||
= render 'report'
|
||||
= render 'score'
|
||||
|
||||
|
|
|
@ -18,17 +18,19 @@
|
|||
= sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
|
||||
.dropdown-content{ data: { testid: 'issue-type-select-dropdown' } }
|
||||
%ul
|
||||
%li.js-filter-issuable-type
|
||||
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
|
||||
#{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_("Issue")}
|
||||
%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")}
|
||||
- if create_issue_type_allowed?(@project, :issue)
|
||||
%li.js-filter-issuable-type
|
||||
= link_to new_project_issue_path(@project), class: ("is-active" if issuable.issue?) do
|
||||
#{sprite_icon(work_item_type_icon(:issue), css_class: 'gl-icon')} #{_('Issue')}
|
||||
- if create_issue_type_allowed?(@project, :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
|
||||
|
||||
- if issuable.incident?
|
||||
%p.form-text.text-muted
|
||||
- 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 }
|
||||
= _('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 }
|
||||
- incident_docs_start = format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: incident_docs_url)
|
||||
= 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
|
||||
|
|
|
@ -7,6 +7,8 @@ module AuthorizedProjectUpdate
|
|||
data_consistency :always
|
||||
include Gitlab::ExclusiveLeaseHelpers
|
||||
|
||||
prepend WaitableWorker
|
||||
|
||||
feature_category :authentication_and_authorization
|
||||
urgency :high
|
||||
queue_namespace :authorized_project_update
|
||||
|
|
|
@ -159,7 +159,10 @@ module ContainerExpirationPolicies
|
|||
|
||||
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
|
||||
|
||||
def log_truncate(result)
|
||||
|
|
|
@ -16,6 +16,8 @@ Bundler.require(*Rails.groups)
|
|||
|
||||
module Gitlab
|
||||
class Application < Rails::Application
|
||||
config.load_defaults 6.1
|
||||
|
||||
require_dependency Rails.root.join('lib/gitlab')
|
||||
require_dependency Rails.root.join('lib/gitlab/utils')
|
||||
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/patch/legacy_database_config')
|
||||
|
||||
config.autoloader = :zeitwerk
|
||||
|
||||
# To be removed in 15.0
|
||||
# This preload is needed to convert legacy `database.yml`
|
||||
# from `production: adapter: postgresql`
|
||||
|
@ -190,11 +190,12 @@ module Gitlab
|
|||
# regardless if schema_search_path is set, or not.
|
||||
config.active_record.dump_schemas = :all
|
||||
|
||||
# Use new connection handling so that we can use Rails 6.1+ multiple
|
||||
# database support.
|
||||
config.active_record.legacy_connection_handling = false
|
||||
|
||||
config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
|
||||
# Override default Active Record settings
|
||||
# We cannot do this in an initializer because some models are already loaded by then
|
||||
config.active_record.cache_versioning = false
|
||||
config.active_record.collection_cache_versioning = false
|
||||
config.active_record.has_many_inversing = false
|
||||
config.active_record.belongs_to_required_by_default = false
|
||||
|
||||
# Enable the asset pipeline
|
||||
config.assets.enabled = true
|
||||
|
@ -380,6 +381,7 @@ module Gitlab
|
|||
config.cache_store = :redis_cache_store, Gitlab::Redis::Cache.active_support_config
|
||||
|
||||
config.active_job.queue_adapter = :sidekiq
|
||||
config.action_mailer.deliver_later_queue_name = :mailers
|
||||
|
||||
# This is needed for gitlab-shell
|
||||
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
|
||||
|
|
|
@ -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
|
|
@ -2,6 +2,5 @@
|
|||
|
||||
# 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 =
|
||||
Gitlab::Utils.to_boolean(ENV['USE_UNSAFE_HYBRID_COOKIES']) ? :hybrid : :json
|
||||
|
|
|
@ -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
|
|
@ -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
|
112
config/plugins/graphql_known_operations_plugin.js
Normal file
112
config/plugins/graphql_known_operations_plugin.js
Normal 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;
|
|
@ -24,6 +24,7 @@ const IS_JH = require('./helpers/is_jh_env');
|
|||
const vendorDllHash = require('./helpers/vendor_dll_hash');
|
||||
|
||||
const MonacoWebpackPlugin = require('./plugins/monaco_webpack');
|
||||
const GraphqlKnownOperationsPlugin = require('./plugins/graphql_known_operations_plugin');
|
||||
|
||||
const ROOT_PATH = path.resolve(__dirname, '..');
|
||||
const SUPPORTED_BROWSERS = fs.readFileSync(path.join(ROOT_PATH, '.browserslistrc'), 'utf-8');
|
||||
|
@ -456,6 +457,8 @@ module.exports = {
|
|||
globalAPI: true,
|
||||
}),
|
||||
|
||||
new GraphqlKnownOperationsPlugin({ filename: 'graphql_known_operations.yml' }),
|
||||
|
||||
// fix legacy jQuery plugins which depend on globals
|
||||
new webpack.ProvidePlugin({
|
||||
$: 'jquery',
|
||||
|
|
|
@ -1298,6 +1298,7 @@ docker build:
|
|||
|
||||
- `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).
|
||||
- `changes` resolves to `true` if any of the matching files are changed (an `OR` operation).
|
||||
|
||||
#### `rules:exists`
|
||||
|
||||
|
@ -1330,6 +1331,7 @@ job:
|
|||
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
|
||||
than 10,000 files.
|
||||
- `exists` resolves to `true` if any of the listed files are found (an `OR` operation).
|
||||
|
||||
#### `rules:allow_failure`
|
||||
|
||||
|
@ -1567,7 +1569,7 @@ docker build:
|
|||
|
||||
**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`,
|
||||
`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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
### 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
|
||||
|
||||
The GitLab test suite is [monitored](performance.md#rspec-profiling) for the `main` branch, and any branch
|
||||
|
|
|
@ -85,6 +85,7 @@ the Agent in subsequent steps.
|
|||
|
||||
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. Select the **GitLab Agent managed clusters** tab.
|
||||
1. Select **Integrate with the GitLab Agent**.
|
||||
|
|
|
@ -29,13 +29,12 @@ Learn more about how GitLab can help you run [Infrastructure as Code](iac/index.
|
|||
|
||||
## Integrated Kubernetes management
|
||||
|
||||
GitLab has special integrations with Kubernetes to help you deploy, manage and troubleshoot
|
||||
third-party or custom applications in Kubernetes clusters. Auto DevOps provides a full
|
||||
DevSecOps pipeline by default targeted at Kubernetes based deployments. To support
|
||||
all the GitLab features, GitLab offers a cluster management project for easy onboarding.
|
||||
The deploy boards provide quick insights into your cluster, including pod logs tailing.
|
||||
The GitLab integration with Kubernetes helps you to install, configure, manage, deploy, and troubleshoot
|
||||
cluster applications. With the GitLab Kubernetes Agent, you can connect clusters behind a firewall,
|
||||
have real-time access to API endpoints, perform pull-beased or push-based deployments for production
|
||||
and non-production environments, and much more.
|
||||
|
||||
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
|
||||
|
||||
|
|
40
lib/gitlab/graphql/known_operations.rb
Normal file
40
lib/gitlab/graphql/known_operations.rb
Normal 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
|
|
@ -6,11 +6,13 @@ module Gitlab
|
|||
module SidekiqConfig
|
||||
FOSS_QUEUE_CONFIG_PATH = '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'
|
||||
|
||||
QUEUE_CONFIG_PATHS = [
|
||||
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
|
||||
|
||||
# This maps workers not in our application code to queues. We need
|
||||
|
@ -33,7 +35,7 @@ module Gitlab
|
|||
weight: 2,
|
||||
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
|
||||
include Gitlab::SidekiqConfig::CliMethods
|
||||
|
@ -58,10 +60,14 @@ module Gitlab
|
|||
@workers ||= begin
|
||||
result = []
|
||||
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?
|
||||
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
|
||||
|
||||
result
|
||||
|
@ -69,16 +75,26 @@ module Gitlab
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
# YAML.load_file is OK here as we control the file contents
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
def queues_for_sidekiq_queues_yml
|
||||
|
@ -120,14 +136,14 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def find_workers(root, ee:)
|
||||
def find_workers(root, ee:, jh:)
|
||||
concerns = root.join('concerns').to_s
|
||||
|
||||
Dir[root.join('**', '*.rb')]
|
||||
.reject { |path| path.start_with?(concerns) }
|
||||
.map { |path| worker_from_path(path, root) }
|
||||
.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
|
||||
|
||||
def worker_from_path(path, root)
|
||||
|
|
|
@ -18,6 +18,7 @@ module Gitlab
|
|||
QUEUE_CONFIG_PATHS = begin
|
||||
result = %w[app/workers/all_queues.yml]
|
||||
result << 'ee/app/workers/all_queues.yml' if Gitlab.ee?
|
||||
result << 'jh/app/workers/all_queues.yml' if Gitlab.jh?
|
||||
result
|
||||
end.freeze
|
||||
|
||||
|
|
|
@ -13,15 +13,20 @@ module Gitlab
|
|||
:worker_has_external_dependencies?,
|
||||
to: :klass
|
||||
|
||||
def initialize(klass, ee:)
|
||||
def initialize(klass, ee:, jh: false)
|
||||
@klass = klass
|
||||
@ee = ee
|
||||
@jh = jh
|
||||
end
|
||||
|
||||
def ee?
|
||||
@ee
|
||||
end
|
||||
|
||||
def jh?
|
||||
@jh
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
to_yaml == case other
|
||||
when self.class
|
||||
|
|
65
lib/gitlab/webpack/file_loader.rb
Normal file
65
lib/gitlab/webpack/file_loader.rb
Normal 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
|
25
lib/gitlab/webpack/graphql_known_operations.rb
Normal file
25
lib/gitlab/webpack/graphql_known_operations.rb
Normal 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
|
|
@ -1,8 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'net/http'
|
||||
require 'uri'
|
||||
|
||||
module Gitlab
|
||||
module Webpack
|
||||
class Manifest
|
||||
|
@ -78,49 +75,16 @@ module Gitlab
|
|||
end
|
||||
|
||||
def load_manifest
|
||||
data = if Gitlab.config.webpack.dev_server.enabled
|
||||
load_dev_server_manifest
|
||||
else
|
||||
load_static_manifest
|
||||
end
|
||||
data = Gitlab::Webpack::FileLoader.load(Gitlab.config.webpack.manifest_filename)
|
||||
|
||||
Gitlab::Json.parse(data)
|
||||
end
|
||||
|
||||
def load_dev_server_manifest
|
||||
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
|
||||
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)
|
||||
rescue Gitlab::Webpack::FileLoader::DevServerSSLError => e
|
||||
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)
|
||||
rescue StandardError => 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)
|
||||
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}"
|
||||
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 Gitlab::Webpack::FileLoader::DevServerLoadError => 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
|
||||
end
|
||||
end
|
||||
|
|
|
@ -100,7 +100,7 @@ module Sidebars
|
|||
::Sidebars::MenuItem.new(
|
||||
title: _('Google Cloud'),
|
||||
link: project_google_cloud_index_path(context.project),
|
||||
active_routes: {},
|
||||
active_routes: { controller: :google_cloud },
|
||||
item_id: :google_cloud
|
||||
)
|
||||
end
|
||||
|
|
|
@ -36,13 +36,17 @@ namespace :gitlab do
|
|||
# Do not edit it manually!
|
||||
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)
|
||||
|
||||
if Gitlab.ee?
|
||||
write_yaml(Gitlab::SidekiqConfig::EE_QUEUE_CONFIG_PATH, banner, ee_workers)
|
||||
end
|
||||
|
||||
if Gitlab.jh?
|
||||
write_yaml(Gitlab::SidekiqConfig::JH_QUEUE_CONFIG_PATH, banner, jh_workers)
|
||||
end
|
||||
end
|
||||
|
||||
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::EE_QUEUE_CONFIG_PATH}
|
||||
#{"- " + Gitlab::SidekiqConfig::JH_QUEUE_CONFIG_PATH if Gitlab.jh?}
|
||||
|
||||
MSG
|
||||
end
|
||||
|
|
|
@ -4037,6 +4037,9 @@ msgid_plural "ApplicationSettings|Approve %d users"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "ApplicationSettings|Approve users"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Approve users in the pending approval status?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -4045,6 +4048,9 @@ msgid_plural "ApplicationSettings|By making this change, you will automatically
|
|||
msgstr[0] ""
|
||||
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"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -227,10 +227,10 @@ GEM
|
|||
watir (6.19.1)
|
||||
regexp_parser (>= 1.2, < 3)
|
||||
selenium-webdriver (>= 3.142.7)
|
||||
webdrivers (4.7.0)
|
||||
webdrivers (5.0.0)
|
||||
nokogiri (~> 1.6)
|
||||
rubyzip (>= 1.3.0)
|
||||
selenium-webdriver (> 3.141, < 5.0)
|
||||
selenium-webdriver (~> 4.0)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.4.2)
|
||||
|
@ -263,10 +263,10 @@ DEPENDENCIES
|
|||
rspec-retry (~> 0.6.1)
|
||||
rspec_junit_formatter (~> 0.4.1)
|
||||
ruby-debug-ide (~> 0.7.0)
|
||||
selenium-webdriver (~> 4.0.0.rc1)
|
||||
selenium-webdriver (~> 4.0)
|
||||
timecop (~> 0.9.1)
|
||||
webdrivers (~> 4.6)
|
||||
webdrivers (~> 5.0)
|
||||
zeitwerk (~> 2.4)
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.29
|
||||
2.2.30
|
||||
|
|
|
@ -166,6 +166,7 @@ function rspec_paralellized_job() {
|
|||
export SUITE_FLAKY_RSPEC_REPORT_PATH="${FLAKY_RSPEC_SUITE_REPORT_PATH}"
|
||||
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 SKIPPED_FLAKY_TESTS_REPORT_PATH="rspec_flaky/skipped_flaky_tests_${report_name}_report.txt"
|
||||
|
||||
if [[ ! -f $FLAKY_RSPEC_REPORT_PATH ]]; then
|
||||
echo "{}" > "${FLAKY_RSPEC_REPORT_PATH}"
|
||||
|
|
29
spec/features/graphql_known_operations_spec.rb
Normal file
29
spec/features/graphql_known_operations_spec.rb
Normal 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
|
|
@ -4,25 +4,29 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe 'New/edit issue', :js do
|
||||
include ActionView::Helpers::JavaScriptHelper
|
||||
include FormHelper
|
||||
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user)}
|
||||
let_it_be(:user2) { create(:user)}
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:user2) { create(:user) }
|
||||
let_it_be(:milestone) { create(:milestone, project: project) }
|
||||
let_it_be(:label) { 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(:current_user) { user }
|
||||
|
||||
before_all do
|
||||
project.add_maintainer(user)
|
||||
project.add_maintainer(user2)
|
||||
end
|
||||
|
||||
before do
|
||||
stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
|
||||
|
||||
project.add_maintainer(user)
|
||||
project.add_maintainer(user2)
|
||||
sign_in(user)
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
context 'new issue' do
|
||||
describe 'new issue' do
|
||||
before do
|
||||
visit new_project_issue_path(project)
|
||||
end
|
||||
|
@ -235,29 +239,42 @@ RSpec.describe 'New/edit issue', :js do
|
|||
end
|
||||
|
||||
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
|
||||
page.within('.issue-form') do
|
||||
click_button 'Issue'
|
||||
end
|
||||
end
|
||||
|
||||
it 'correctly displays the Issue type option with an icon', :aggregate_failures do
|
||||
page.within('[data-testid="issue-type-select-dropdown"]') do
|
||||
expect(page).to have_selector('[data-testid="issue-type-issue-icon"]')
|
||||
expect(page).to have_content('Issue')
|
||||
end
|
||||
end
|
||||
it_behaves_like 'type option is visible', label: 'Issue', identifier: :issue
|
||||
it_behaves_like 'type option is visible', label: 'Incident', identifier: :incident
|
||||
|
||||
it 'correctly displays the Incident type option with an icon', :aggregate_failures do
|
||||
page.within('[data-testid="issue-type-select-dropdown"]') do
|
||||
expect(page).to have_selector('[data-testid="issue-type-incident-icon"]')
|
||||
expect(page).to have_content('Incident')
|
||||
context 'when user is guest' do
|
||||
let_it_be(:guest) { create(:user) }
|
||||
|
||||
let(:current_user) { guest }
|
||||
|
||||
before_all do
|
||||
project.add_guest(guest)
|
||||
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
|
||||
|
||||
describe 'milestone' do
|
||||
let!(:milestone) { create(:milestone, title: '"><img src=x onerror=alert(document.domain)>', project: project) }
|
||||
let!(:milestone) do
|
||||
create(:milestone, title: '"><img src=x onerror=alert(document.domain)>', project: project)
|
||||
end
|
||||
|
||||
it 'escapes milestone' do
|
||||
click_button 'Milestone'
|
||||
|
@ -274,7 +291,7 @@ RSpec.describe 'New/edit issue', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'edit issue' do
|
||||
describe 'edit issue' do
|
||||
before do
|
||||
visit edit_project_issue_path(project, issue)
|
||||
end
|
||||
|
@ -329,7 +346,7 @@ RSpec.describe 'New/edit issue', :js do
|
|||
end
|
||||
end
|
||||
|
||||
context 'inline edit' do
|
||||
describe 'inline edit' do
|
||||
before do
|
||||
visit project_issue_path(project, issue)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { GlBanner } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
|
||||
import { INTRO_COOKIE_KEY } from '~/analytics/devops_report/constants';
|
||||
import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
|
||||
import { INTRO_COOKIE_KEY } from '~/analytics/devops_reports/constants';
|
||||
import * as utils from '~/lib/utils/common_utils';
|
||||
import { devopsReportDocsPath, devopsScoreIntroImagePath } from '../mock_data';
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@ import { GlTable, GlBadge, GlEmptyState } from '@gitlab/ui';
|
|||
import { GlSingleStat } from '@gitlab/ui/dist/charts';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import DevopsScore from '~/analytics/devops_report/components/devops_score.vue';
|
||||
import DevopsScoreCallout from '~/analytics/devops_report/components/devops_score_callout.vue';
|
||||
import DevopsScore from '~/analytics/devops_reports/components/devops_score.vue';
|
||||
import DevopsScoreCallout from '~/analytics/devops_reports/components/devops_score_callout.vue';
|
||||
import { devopsScoreMetricsData, noDataImagePath, devopsScoreTableHeaders } from '../mock_data';
|
||||
|
||||
describe('DevopsScore', () => {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { GlEmptyState, GlSprintf } from '@gitlab/ui';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
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;
|
||||
|
||||
afterEach(() => {
|
|
@ -1,6 +1,7 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
|
||||
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
|
||||
import { createStore } from '~/mr_notes/stores';
|
||||
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
||||
import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
|
||||
|
@ -19,6 +20,9 @@ describe('DiffDiscussions', () => {
|
|||
store = createStore();
|
||||
wrapper = mount(localVue.extend(DiffDiscussions), {
|
||||
store,
|
||||
provide: {
|
||||
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||
},
|
||||
propsData: {
|
||||
discussions: getDiscussionsMockData(),
|
||||
...props,
|
||||
|
|
133
spec/frontend/diffs/utils/discussions_spec.js
Normal file
133
spec/frontend/diffs/utils/discussions_spec.js
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,6 +1,7 @@
|
|||
import { getByRole } from '@testing-library/dom';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import '~/behaviors/markdown/render_gfm';
|
||||
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
|
||||
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
||||
import NoteableNote from '~/notes/components/noteable_note.vue';
|
||||
import { SYSTEM_NOTE } from '~/notes/constants';
|
||||
|
@ -26,6 +27,9 @@ describe('DiscussionNotes', () => {
|
|||
const createComponent = (props, mountingMethod = shallowMount) => {
|
||||
wrapper = mountingMethod(DiscussionNotes, {
|
||||
store,
|
||||
provide: {
|
||||
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||
},
|
||||
propsData: {
|
||||
discussion: discussionMock,
|
||||
isExpanded: false,
|
||||
|
|
|
@ -3,6 +3,7 @@ import { nextTick } from 'vue';
|
|||
import discussionWithTwoUnresolvedNotes from 'test_fixtures/merge_requests/resolved_diff_discussion.json';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import mockDiffFile from 'jest/diffs/mock_data/diff_file';
|
||||
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
|
||||
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
|
||||
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
|
||||
|
@ -31,6 +32,9 @@ describe('noteable_discussion component', () => {
|
|||
|
||||
wrapper = mount(NoteableDiscussion, {
|
||||
store,
|
||||
provide: {
|
||||
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||
},
|
||||
propsData: { discussion: discussionMock },
|
||||
});
|
||||
});
|
||||
|
@ -167,6 +171,9 @@ describe('noteable_discussion component', () => {
|
|||
|
||||
wrapper = mount(NoteableDiscussion, {
|
||||
store,
|
||||
provide: {
|
||||
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||
},
|
||||
propsData: { discussion: discussionMock },
|
||||
});
|
||||
});
|
||||
|
@ -185,6 +192,9 @@ describe('noteable_discussion component', () => {
|
|||
|
||||
wrapper = mount(NoteableDiscussion, {
|
||||
store,
|
||||
provide: {
|
||||
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||
},
|
||||
propsData: { discussion: discussionMock },
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import DraftNote from '~/batch_comments/components/draft_note.vue';
|
|||
import batchComments from '~/batch_comments/stores/modules/batch_comments';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as urlUtility from '~/lib/utils/url_utility';
|
||||
import { discussionIntersectionObserverHandlerFactory } from '~/diffs/utils/discussions';
|
||||
import CommentForm from '~/notes/components/comment_form.vue';
|
||||
import NotesApp from '~/notes/components/notes_app.vue';
|
||||
import * as constants from '~/notes/constants';
|
||||
|
@ -78,6 +79,9 @@ describe('note_app', () => {
|
|||
</div>`,
|
||||
},
|
||||
{
|
||||
provide: {
|
||||
discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
|
||||
},
|
||||
propsData,
|
||||
store,
|
||||
},
|
||||
|
|
72
spec/lib/gitlab/graphql/known_operations_spec.rb
Normal file
72
spec/lib/gitlab/graphql/known_operations_spec.rb
Normal 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
|
|
@ -11,12 +11,12 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
|
|||
end
|
||||
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
def stub_contents(foss_queues, ee_queues)
|
||||
def stub_contents(foss_queues, ee_queues, jh_queues)
|
||||
allow(YAML).to receive(:load_file)
|
||||
.with(expand_path('app/workers/all_queues.yml'))
|
||||
.and_return(foss_queues)
|
||||
|
@ -24,6 +24,10 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
|
|||
allow(YAML).to receive(:load_file)
|
||||
.with(expand_path('ee/app/workers/all_queues.yml'))
|
||||
.and_return(ee_queues)
|
||||
|
||||
allow(YAML).to receive(:load_file)
|
||||
.with(expand_path('jh/app/workers/all_queues.yml'))
|
||||
.and_return(jh_queues)
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -45,8 +49,9 @@ RSpec.describe Gitlab::SidekiqConfig::CliMethods do
|
|||
end
|
||||
|
||||
it 'flattens and joins the contents' do
|
||||
expected_queues = %w[queue_a queue_b]
|
||||
expected_queues = expected_queues.first(1) unless Gitlab.ee?
|
||||
expected_queues = %w[queue_a]
|
||||
expected_queues << 'queue_b' if Gitlab.ee?
|
||||
expected_queues << 'queue_c' if Gitlab.jh?
|
||||
|
||||
expect(described_class.worker_queues(dummy_root))
|
||||
.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
|
||||
before do
|
||||
stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }])
|
||||
stub_contents([{ name: 'queue_a' }], [{ name: 'queue_b' }], [{ name: 'queue_c' }])
|
||||
end
|
||||
|
||||
include_examples 'valid file contents'
|
||||
|
|
|
@ -18,19 +18,26 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
|
|||
get_tags: attributes[:tags]
|
||||
)
|
||||
|
||||
described_class.new(inner_worker, ee: false)
|
||||
described_class.new(inner_worker, ee: false, jh: false)
|
||||
end
|
||||
|
||||
describe '#ee?' 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: false)).not_to be_ee
|
||||
expect(described_class.new(double, ee: true, jh: false)).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
|
||||
|
||||
describe '#==' do
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
@ -57,7 +64,7 @@ RSpec.describe Gitlab::SidekiqConfig::Worker do
|
|||
|
||||
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
|
||||
|
|
79
spec/lib/gitlab/webpack/file_loader_spec.rb
Normal file
79
spec/lib/gitlab/webpack/file_loader_spec.rb
Normal 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
|
47
spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
Normal file
47
spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
Normal 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
|
|
@ -51,6 +51,16 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
|
|||
it 'menu link points to Terraform page' do
|
||||
expect(subject.link).to eq find_menu_item(:terraform).link
|
||||
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
|
||||
|
||||
|
@ -89,5 +99,11 @@ RSpec.describe Sidebars::Projects::Menus::InfrastructureMenu do
|
|||
|
||||
it_behaves_like 'access rights checks'
|
||||
end
|
||||
|
||||
describe 'Google Cloud' do
|
||||
let(:item_id) { :google_cloud }
|
||||
|
||||
it_behaves_like 'access rights checks'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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(:new_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
|
||||
new_parent_group.add_maintainer(new_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
|
||||
subgroup1.refresh_members_authorized_projects
|
||||
end
|
||||
|
||||
it 'removes old project authorizations' do
|
||||
|
@ -613,7 +618,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
|
|||
end
|
||||
|
||||
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)
|
||||
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
|
||||
}.from(0).to(1)
|
||||
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
|
||||
|
||||
context 'for groups with many members' do
|
||||
before do
|
||||
11.times do
|
||||
new_parent_group.add_maintainer(create(:user))
|
||||
end
|
||||
context 'for nested groups with unique members' do
|
||||
it 'preserves existing project authorizations' do
|
||||
expect { transfer_service.execute(new_parent_group) }.not_to change {
|
||||
ProjectAuthorization.where(project_id: nested_project.id, user_id: unique_subgroup_member.id).count
|
||||
}
|
||||
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
|
||||
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)
|
||||
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
|
||||
expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async)
|
||||
.with(array_including(new_parent_group.members_with_parents.pluck(:user_id).map {|id| [id, anything] }))
|
||||
expect(AuthorizedProjectUpdate::ProjectRecalculateWorker).to receive(:bulk_perform_async)
|
||||
.with(array_including(group.all_projects.ids.map { |id| [id, anything] }))
|
||||
|
||||
transfer_service.execute(new_parent_group)
|
||||
end
|
||||
|
|
|
@ -107,9 +107,7 @@ RSpec.configure do |config|
|
|||
warn `curl -s -o log/goroutines.log http://localhost:9236/debug/pprof/goroutine?debug=2`
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless ENV['CI']
|
||||
else
|
||||
# Allow running `:focus` examples locally,
|
||||
# falling back to all tests when there is no `:focus` example.
|
||||
config.filter_run focus: true
|
||||
|
|
36
spec/support/flaky_tests.rb
Normal file
36
spec/support/flaky_tests.rb
Normal 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
|
|
@ -82,8 +82,9 @@ RSpec.describe ContainerExpirationPolicies::CleanupContainerRepositoryWorker do
|
|||
nil | 10 | nil
|
||||
0 | 5 | nil
|
||||
10 | 0 | 0
|
||||
10 | 5 | 0.5
|
||||
3 | 10 | (10 / 3.to_f)
|
||||
10 | 5 | 50.0
|
||||
17 | 3 | 17.65
|
||||
3 | 10 | 333.33
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
Loading…
Reference in a new issue