diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 478fd01f541..8f3064c55f6 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -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 #
#######################################################
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 3e9d977cd83..91320b8988f 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -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 #
#########################
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 016a2771cc5..c104cc574c6 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-7b9cd199b0851fd1b6615e0798f2aafddafd63cb
+460a880c6993ab5f76cac951fccc02efd5cbd444
diff --git a/Gemfile b/Gemfile
index 109c3c1cf5b..4ef4501b909 100644
--- a/Gemfile
+++ b/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'
diff --git a/Gemfile.lock b/Gemfile.lock
index 339579dccc7..7f1bf38a9c4 100644
--- a/Gemfile.lock
+++ b/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)
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score.vue b/app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
similarity index 100%
rename from app/assets/javascripts/analytics/devops_report/components/devops_score.vue
rename to app/assets/javascripts/analytics/devops_reports/components/devops_score.vue
diff --git a/app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue b/app/assets/javascripts/analytics/devops_reports/components/devops_score_callout.vue
similarity index 100%
rename from app/assets/javascripts/analytics/devops_report/components/devops_score_callout.vue
rename to app/assets/javascripts/analytics/devops_reports/components/devops_score_callout.vue
diff --git a/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue b/app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
similarity index 100%
rename from app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue
rename to app/assets/javascripts/analytics/devops_reports/components/service_ping_disabled.vue
diff --git a/app/assets/javascripts/analytics/devops_report/constants.js b/app/assets/javascripts/analytics/devops_reports/constants.js
similarity index 100%
rename from app/assets/javascripts/analytics/devops_report/constants.js
rename to app/assets/javascripts/analytics/devops_reports/constants.js
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score.js b/app/assets/javascripts/analytics/devops_reports/devops_score.js
similarity index 100%
rename from app/assets/javascripts/analytics/devops_report/devops_score.js
rename to app/assets/javascripts/analytics/devops_reports/devops_score.js
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js b/app/assets/javascripts/analytics/devops_reports/devops_score_disabled_service_ping.js
similarity index 100%
rename from app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js
rename to app/assets/javascripts/analytics/devops_reports/devops_score_disabled_service_ping.js
diff --git a/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql
deleted file mode 100644
index 1c382c4747b..00000000000
--- a/app/assets/javascripts/boards/graphql/group_board_iterations.query.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-query GroupBoardIterations($fullPath: ID!, $title: String) {
- group(fullPath: $fullPath) {
- iterations(includeAncestors: true, title: $title) {
- nodes {
- id
- title
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql b/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql
deleted file mode 100644
index 078151a275a..00000000000
--- a/app/assets/javascripts/boards/graphql/project_board_iterations.query.graphql
+++ /dev/null
@@ -1,10 +0,0 @@
-query ProjectBoardIterations($fullPath: ID!, $title: String) {
- project(fullPath: $fullPath) {
- iterations(includeAncestors: true, title: $title) {
- nodes {
- id
- title
- }
- }
- }
-}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index c81e3cb79db..3a96e535cf7 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -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);
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
index 928cece19f7..31b78014525 100644
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ b/app/assets/javascripts/boards/stores/mutation_types.js
@@ -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';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index ef5b84b4575..2a2ce7652e6 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -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;
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 66d06a3a1b6..f405b82b05b 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -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,
diff --git a/app/assets/javascripts/diffs/utils/discussions.js b/app/assets/javascripts/diffs/utils/discussions.js
new file mode 100644
index 00000000000..c404705d209
--- /dev/null
+++ b/app/assets/javascripts/diffs/utils/discussions.js
@@ -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);
+ };
+}
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index 6fcfa66ea49..d1df4eb848b 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -1,5 +1,6 @@
@@ -122,33 +152,35 @@ export default {
@mouseleave="handleMouseLeave(discussion)"
>
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
{ 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
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5c307158a9a..4273eb331a1 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -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
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index bd080f5c479..12041b103f6 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -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) }
diff --git a/app/services/authorized_project_update/project_access_changed_service.rb b/app/services/authorized_project_update/project_access_changed_service.rb
new file mode 100644
index 00000000000..62bf4ced1ae
--- /dev/null
+++ b/app/services/authorized_project_update/project_access_changed_service.rb
@@ -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
diff --git a/app/services/concerns/issues/issue_type_helpers.rb b/app/services/concerns/issues/issue_type_helpers.rb
new file mode 100644
index 00000000000..44c20d20ff1
--- /dev/null
+++ b/app/services/concerns/issues/issue_type_helpers.rb
@@ -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
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index 7e572cbb79b..cd89eb799dc 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -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)
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 6dce9fd6e73..efb5de5b17c 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -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)
diff --git a/app/services/issues/build_service.rb b/app/services/issues/build_service.rb
index 7fdc8daf15c..8fd844c4886 100644
--- a/app/services/issues/build_service.rb
+++ b/app/services/issues/build_service.rb
@@ -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
diff --git a/app/views/admin/dev_ops_report/_report.html.haml b/app/views/admin/dev_ops_report/_score.html.haml
similarity index 100%
rename from app/views/admin/dev_ops_report/_report.html.haml
rename to app/views/admin/dev_ops_report/_score.html.haml
diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml
index f8fc7b42691..a2425b93ad3 100644
--- a/app/views/admin/dev_ops_report/show.html.haml
+++ b/app/views/admin/dev_ops_report/show.html.haml
@@ -6,5 +6,5 @@
- if show_adoption?
= render_if_exists 'admin/dev_ops_report/devops_tabs'
- else
- = render 'report'
+ = render 'score'
diff --git a/app/views/shared/issuable/form/_type_selector.html.haml b/app/views/shared/issuable/form/_type_selector.html.haml
index f5f6f32d5ba..ae0fe54de4f 100644
--- a/app/views/shared/issuable/form/_type_selector.html.haml
+++ b/app/views/shared/issuable/form/_type_selector.html.haml
@@ -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 = ''.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: ''.html_safe }
+ - incident_docs_start = format('', 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: '').html_safe
diff --git a/app/workers/authorized_project_update/project_recalculate_worker.rb b/app/workers/authorized_project_update/project_recalculate_worker.rb
index 3d073f18622..1b5faee0b6f 100644
--- a/app/workers/authorized_project_update/project_recalculate_worker.rb
+++ b/app/workers/authorized_project_update/project_recalculate_worker.rb
@@ -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
diff --git a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
index 69f5906f54c..7f7a77d0524 100644
--- a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
+++ b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
@@ -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)
diff --git a/config/application.rb b/config/application.rb
index eb84b274c6b..1fc963a1c1e 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -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']
diff --git a/config/initializers/action_view.rb b/config/initializers/action_view.rb
deleted file mode 100644
index 76171733483..00000000000
--- a/config/initializers/action_view.rb
+++ /dev/null
@@ -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
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
index 375a23fdfd6..27451001679 100644
--- a/config/initializers/cookies_serializer.rb
+++ b/config/initializers/cookies_serializer.rb
@@ -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
diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb
deleted file mode 100644
index 3d3ee5299e2..00000000000
--- a/config/initializers/new_framework_defaults.rb
+++ /dev/null
@@ -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
diff --git a/config/initializers_before_autoloader/000_override_framework_defaults.rb b/config/initializers_before_autoloader/000_override_framework_defaults.rb
new file mode 100644
index 00000000000..3f8faa78d41
--- /dev/null
+++ b/config/initializers_before_autoloader/000_override_framework_defaults.rb
@@ -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
diff --git a/config/plugins/graphql_known_operations_plugin.js b/config/plugins/graphql_known_operations_plugin.js
new file mode 100644
index 00000000000..164b34c1dd1
--- /dev/null
+++ b/config/plugins/graphql_known_operations_plugin.js
@@ -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;
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 7f220b584a3..f334e17bbaf 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -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',
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index e6a5625a827..4a41b0cbaa6 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -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.
diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md
index a198526dd09..802d3838fe1 100644
--- a/doc/development/pipelines.md
+++ b/doc/development/pipelines.md
@@ -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
diff --git a/doc/user/clusters/agent/install/index.md b/doc/user/clusters/agent/install/index.md
index f4883221bd7..ca5382647ba 100644
--- a/doc/user/clusters/agent/install/index.md
+++ b/doc/user/clusters/agent/install/index.md
@@ -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**.
diff --git a/doc/user/infrastructure/index.md b/doc/user/infrastructure/index.md
index e99dc691774..3dcce47105b 100644
--- a/doc/user/infrastructure/index.md
+++ b/doc/user/infrastructure/index.md
@@ -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
diff --git a/lib/gitlab/graphql/known_operations.rb b/lib/gitlab/graphql/known_operations.rb
new file mode 100644
index 00000000000..954efa5a933
--- /dev/null
+++ b/lib/gitlab/graphql/known_operations.rb
@@ -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
diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb
index 5663c51bb7a..07ddac209f8 100644
--- a/lib/gitlab/sidekiq_config.rb
+++ b/lib/gitlab/sidekiq_config.rb
@@ -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)
diff --git a/lib/gitlab/sidekiq_config/cli_methods.rb b/lib/gitlab/sidekiq_config/cli_methods.rb
index 8eef15f9ccb..70798f8c3e8 100644
--- a/lib/gitlab/sidekiq_config/cli_methods.rb
+++ b/lib/gitlab/sidekiq_config/cli_methods.rb
@@ -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
diff --git a/lib/gitlab/sidekiq_config/worker.rb b/lib/gitlab/sidekiq_config/worker.rb
index a343573440f..1e3fb675ca7 100644
--- a/lib/gitlab/sidekiq_config/worker.rb
+++ b/lib/gitlab/sidekiq_config/worker.rb
@@ -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
diff --git a/lib/gitlab/webpack/file_loader.rb b/lib/gitlab/webpack/file_loader.rb
new file mode 100644
index 00000000000..35ecb1eb4ed
--- /dev/null
+++ b/lib/gitlab/webpack/file_loader.rb
@@ -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
diff --git a/lib/gitlab/webpack/graphql_known_operations.rb b/lib/gitlab/webpack/graphql_known_operations.rb
new file mode 100644
index 00000000000..7945513667c
--- /dev/null
+++ b/lib/gitlab/webpack/graphql_known_operations.rb
@@ -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
diff --git a/lib/gitlab/webpack/manifest.rb b/lib/gitlab/webpack/manifest.rb
index b73c2ebb578..06cddc23134 100644
--- a/lib/gitlab/webpack/manifest.rb
+++ b/lib/gitlab/webpack/manifest.rb
@@ -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
diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb
index c0c1b9b195c..ccc4787601a 100644
--- a/lib/sidebars/projects/menus/infrastructure_menu.rb
+++ b/lib/sidebars/projects/menus/infrastructure_menu.rb
@@ -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
diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake
index 90ed91221ae..2e383065b64 100644
--- a/lib/tasks/gitlab/sidekiq.rake
+++ b/lib/tasks/gitlab/sidekiq.rake
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4752b246616..b9709b62906 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 48511b59e64..0332fc9a13a 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -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
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index a3565db58fa..51ccf9895c2 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -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}"
diff --git a/spec/features/graphql_known_operations_spec.rb b/spec/features/graphql_known_operations_spec.rb
new file mode 100644
index 00000000000..ef406f12902
--- /dev/null
+++ b/spec/features/graphql_known_operations_spec.rb
@@ -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
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 4bad67acc87..bb68dcb614a 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -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
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
index ee14e002f1b..c9a899ab78b 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_callout_spec.js
@@ -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';
diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
index 8f8dac977de..824eb033671 100644
--- a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
+++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js
@@ -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', () => {
diff --git a/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
similarity index 90%
rename from spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js
rename to spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
index c5c40e9a360..c62bfb11f7b 100644
--- a/spec/frontend/analytics/devops_report/components/service_ping_disabled_spec.js
+++ b/spec/frontend/analytics/devops_reports/components/service_ping_disabled_spec.js
@@ -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(() => {
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index bd6f4cd2545..c847a79435a 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -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,
diff --git a/spec/frontend/diffs/utils/discussions_spec.js b/spec/frontend/diffs/utils/discussions_spec.js
new file mode 100644
index 00000000000..9a3d442d943
--- /dev/null
+++ b/spec/frontend/diffs/utils/discussions_spec.js
@@ -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);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 59ac75f00e6..ff840a55535 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -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,
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index 727ef02dcbb..6aab60edc4e 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -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 },
});
});
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index e91767687e8..b3dbc26878f 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -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', () => {
`,
},
{
+ provide: {
+ discussionObserverHandler: discussionIntersectionObserverHandlerFactory(),
+ },
propsData,
store,
},
diff --git a/spec/lib/gitlab/graphql/known_operations_spec.rb b/spec/lib/gitlab/graphql/known_operations_spec.rb
new file mode 100644
index 00000000000..58fa2c18639
--- /dev/null
+++ b/spec/lib/gitlab/graphql/known_operations_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
index bc63289a344..576b36c1829 100644
--- a/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/cli_methods_spec.rb
@@ -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'
diff --git a/spec/lib/gitlab/sidekiq_config/worker_spec.rb b/spec/lib/gitlab/sidekiq_config/worker_spec.rb
index f4d7a4b3359..9c252b3d50b 100644
--- a/spec/lib/gitlab/sidekiq_config/worker_spec.rb
+++ b/spec/lib/gitlab/sidekiq_config/worker_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/webpack/file_loader_spec.rb b/spec/lib/gitlab/webpack/file_loader_spec.rb
new file mode 100644
index 00000000000..34d00b9f106
--- /dev/null
+++ b/spec/lib/gitlab/webpack/file_loader_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb b/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
new file mode 100644
index 00000000000..89cade82fe6
--- /dev/null
+++ b/spec/lib/gitlab/webpack/graphql_known_operations_spec.rb
@@ -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
diff --git a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
index 2415598da9c..55281171634 100644
--- a/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb
@@ -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
diff --git a/spec/services/authorized_project_update/project_access_changed_service_spec.rb b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
new file mode 100644
index 00000000000..11621055a47
--- /dev/null
+++ b/spec/services/authorized_project_update/project_access_changed_service_spec.rb
@@ -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
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 0b032d6a903..21aa213a8ff 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -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
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5de63acf66d..6a0d9ea0669 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -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
diff --git a/spec/support/flaky_tests.rb b/spec/support/flaky_tests.rb
new file mode 100644
index 00000000000..c619f1412f1
--- /dev/null
+++ b/spec/support/flaky_tests.rb
@@ -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
diff --git a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
index d4126fe688a..cbffb8f3870 100644
--- a/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
+++ b/spec/workers/container_expiration_policies/cleanup_container_repository_worker_spec.rb
@@ -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