Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-12-07 03:08:34 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-12-07 03:08:34 +0300
commit7e89568aa1b1c531aa34860fbd9e77d9e988b9b2 (patch)
tree9d644d947b75594d969f040ef046541c769e0dc3
parentf2143c9986ad7b6206b8a41cc9aeb419e543d3f5 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/layout/first_hash_element_indentation.yml16
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue78
-rw-r--r--app/assets/javascripts/issues/dashboard/index.js2
-rw-r--r--app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql2
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue18
-rw-r--r--app/assets/javascripts/issues/list/constants.js20
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js3
-rw-r--r--app/components/diffs/stats_component.rb16
-rw-r--r--app/controllers/admin/ci/variables_controller.rb4
-rw-r--r--app/controllers/admin/system_info_controller.rb10
-rw-r--r--app/controllers/concerns/milestone_actions.rb24
-rw-r--r--app/controllers/concerns/render_service_results.rb24
-rw-r--r--app/controllers/concerns/sourcegraph_decorator.rb4
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb12
-rw-r--r--app/controllers/projects/badges_controller.rb30
-rw-r--r--app/controllers/repositories/lfs_locks_api_controller.rb6
-rw-r--r--app/experiments/concerns/project_commit_count.rb6
-rw-r--r--app/graphql/mutations/clusters/agent_tokens/create.rb6
-rw-r--r--app/graphql/mutations/notes/create/diff_note.rb8
-rw-r--r--app/graphql/mutations/notes/create/image_diff_note.rb6
-rw-r--r--app/graphql/mutations/notes/create/note.rb6
-rw-r--r--app/graphql/mutations/todos/restore_many.rb6
-rw-r--r--app/graphql/resolvers/group_packages_resolver.rb6
-rw-r--r--app/helpers/issues_helper.rb1
-rw-r--r--app/models/integrations/base_slack_notification.rb1
-rw-r--r--app/models/integrations/jira.rb1
-rw-r--r--app/models/project.rb32
-rw-r--r--app/models/project_export_job.rb14
-rw-r--r--app/policies/base_policy.rb13
-rw-r--r--app/policies/merge_request_policy.rb2
-rw-r--r--app/views/dashboard/_activities.html.haml3
-rw-r--r--app/views/dashboard/_projects_head.html.haml3
-rw-r--r--app/views/dashboard/_snippets_head.html.haml3
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/container_registry/migration/enqueuer_worker.rb4
-rw-r--r--app/workers/gitlab/export/prune_project_export_jobs_worker.rb23
-rwxr-xr-xbin/audit-event-type24
-rw-r--r--config/audit_events/types/policy_project_updated.yml2
-rw-r--r--config/audit_events/types/type_schema.json6
-rw-r--r--config/feature_flags/development/cache_project_integrations.yml (renamed from config/feature_flags/development/approval_rules_pagination.yml)12
-rw-r--r--config/feature_flags/ops/purge_stale_security_findings.yml8
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--danger/plugins/stable_branch.rb9
-rw-r--r--danger/stable_branch_patch/Dangerfile3
-rw-r--r--db/post_migrate/20221128120634_schedule_fixing_security_scan_statuses.rb52
-rw-r--r--db/post_migrate/20221129124240_remove_flowdock_integration_records.rb23
-rw-r--r--db/schema_migrations/202211281206341
-rw-r--r--db/schema_migrations/202211291242401
-rw-r--r--doc/api/merge_request_approvals.md2
-rw-r--r--doc/development/code_review.md3
-rw-r--r--lib/api/merge_request_approvals.rb1
-rw-r--r--lib/gitlab/audit/type/shared.rb2
-rw-r--r--lib/gitlab/background_migration/fix_security_scan_statuses.rb14
-rw-r--r--lib/gitlab/import_export/project/import_export.yml17
-rw-r--r--locale/gitlab.pot9
-rw-r--r--qa/qa/support/loglinking.rb6
-rw-r--r--qa/spec/resource/api_fabricator_spec.rb2
-rw-r--r--qa/spec/support/loglinking_spec.rb6
-rw-r--r--spec/bin/audit_event_type_spec.rb30
-rw-r--r--spec/db/schema_spec.rb3
-rw-r--r--spec/factories/projects/import_export/relation_export_upload.rb2
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js143
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js2
-rw-r--r--spec/helpers/issues_helper_spec.rb6
-rw-r--r--spec/lib/gitlab/audit/type/definition_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml14
-rw-r--r--spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb6
-rw-r--r--spec/migrations/remove_flowdock_integration_records_spec.rb23
-rw-r--r--spec/migrations/schedule_fixing_security_scan_statuses_spec.rb76
-rw-r--r--spec/models/integrations/jira_spec.rb4
-rw-r--r--spec/models/project_export_job_spec.rb52
-rw-r--r--spec/models/project_spec.rb41
-rw-r--r--spec/policies/merge_request_policy_spec.rb102
-rw-r--r--spec/requests/api/merge_request_approvals_spec.rb81
-rw-r--r--spec/requests/api/merge_requests_spec.rb80
-rw-r--r--spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb2
-rw-r--r--spec/tooling/danger/stable_branch_spec.rb169
-rw-r--r--spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb52
-rw-r--r--tooling/danger/stable_branch.rb138
80 files changed, 1380 insertions, 283 deletions
diff --git a/.rubocop_todo/layout/first_hash_element_indentation.yml b/.rubocop_todo/layout/first_hash_element_indentation.yml
index 34446f2cb61..2f20d2ec239 100644
--- a/.rubocop_todo/layout/first_hash_element_indentation.yml
+++ b/.rubocop_todo/layout/first_hash_element_indentation.yml
@@ -2,22 +2,6 @@
# Cop supports --autocorrect.
Layout/FirstHashElementIndentation:
Exclude:
- - 'app/components/diffs/stats_component.rb'
- - 'app/controllers/admin/ci/variables_controller.rb'
- - 'app/controllers/admin/system_info_controller.rb'
- - 'app/controllers/concerns/milestone_actions.rb'
- - 'app/controllers/concerns/render_service_results.rb'
- - 'app/controllers/concerns/sourcegraph_decorator.rb'
- - 'app/controllers/profiles/two_factor_auths_controller.rb'
- - 'app/controllers/projects/badges_controller.rb'
- - 'app/controllers/repositories/lfs_locks_api_controller.rb'
- - 'app/experiments/concerns/project_commit_count.rb'
- - 'app/graphql/mutations/clusters/agent_tokens/create.rb'
- - 'app/graphql/mutations/notes/create/diff_note.rb'
- - 'app/graphql/mutations/notes/create/image_diff_note.rb'
- - 'app/graphql/mutations/notes/create/note.rb'
- - 'app/graphql/mutations/todos/restore_many.rb'
- - 'app/graphql/resolvers/group_packages_resolver.rb'
- 'app/helpers/avatars_helper.rb'
- 'app/helpers/breadcrumbs_helper.rb'
- 'app/helpers/broadcast_messages_helper.rb'
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index 080f1fe222c..5e2bd096534 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -5,9 +5,17 @@ import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import { IssuableStatus } from '~/issues/constants';
-import { PAGE_SIZE } from '~/issues/list/constants';
-import { getInitialPageParams } from '~/issues/list/utils';
+import {
+ CREATED_DESC,
+ PAGE_SIZE,
+ PARAM_STATE,
+ UPDATED_DESC,
+ urlSortParams,
+} from '~/issues/list/constants';
+import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
+import { getInitialPageParams, getSortKey, getSortOptions, isSortKey } from '~/issues/list/utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
@@ -17,13 +25,10 @@ export default {
calendarButtonText: __('Subscribe to calendar'),
closed: __('CLOSED'),
closedMoved: __('CLOSED (MOVED)'),
- downvotes: __('Downvotes'),
emptyStateTitle: __('Please select at least one filter to see results'),
errorFetchingIssues: __('An error occurred while loading issues'),
- relatedMergeRequests: __('Related merge requests'),
rssButtonText: __('Subscribe to RSS feed'),
searchInputPlaceholder: __('Search or filter results...'),
- upvotes: __('Upvotes'),
},
IssuableListTabs,
components: {
@@ -39,20 +44,35 @@ export default {
inject: [
'calendarPath',
'emptyStateSvgPath',
+ 'hasBlockedIssuesFeature',
+ 'hasIssuableHealthStatusFeature',
+ 'hasIssueWeightsFeature',
'hasScopedLabelsFeature',
+ 'initialSort',
'isPublicVisibilityRestricted',
'isSignedIn',
'rssPath',
],
data() {
+ const state = getParameterByName(PARAM_STATE);
+
+ const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
+ const dashboardSortKey = getSortKey(this.initialSort);
+ const graphQLSortKey =
+ isSortKey(this.initialSort?.toUpperCase()) && this.initialSort.toUpperCase();
+
+ // The initial sort is an old enum value when it is saved on the dashboard issues page.
+ // The initial sort is a GraphQL enum value when it is saved on the Vue issues list page.
+ const sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
+
return {
issues: [],
issuesError: null,
pageInfo: {},
pageParams: getInitialPageParams(),
searchTokens: [],
- sortOptions: [],
- state: IssuableStates.Opened,
+ sortKey,
+ state: state || IssuableStates.Opened,
};
},
apollo: {
@@ -62,6 +82,7 @@ export default {
return {
hideUsers: this.isPublicVisibilityRestricted && !this.isSignedIn,
isSignedIn: this.isSignedIn,
+ sort: this.sortKey,
state: this.state,
...this.pageParams,
};
@@ -82,6 +103,19 @@ export default {
showPaginationControls() {
return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
},
+ sortOptions() {
+ return getSortOptions({
+ hasBlockedIssuesFeature: this.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: this.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: this.hasIssueWeightsFeature,
+ });
+ },
+ urlParams() {
+ return {
+ sort: urlSortParams[this.sortKey],
+ state: this.state,
+ };
+ },
},
methods: {
getStatus(issue) {
@@ -117,6 +151,33 @@ export default {
};
scrollUp();
},
+ handleSort(sortKey) {
+ if (this.sortKey === sortKey) {
+ return;
+ }
+
+ this.pageParams = getInitialPageParams();
+ this.sortKey = sortKey;
+
+ if (this.isSignedIn) {
+ this.saveSortPreference(sortKey);
+ }
+ },
+ saveSortPreference(sortKey) {
+ this.$apollo
+ .mutate({
+ mutation: setSortPreferenceMutation,
+ variables: { input: { issuesSort: sortKey } },
+ })
+ .then(({ data }) => {
+ if (data.userPreferencesUpdate.errors.length) {
+ throw new Error(data.userPreferencesUpdate.errors);
+ }
+ })
+ .catch((error) => {
+ Sentry.captureException(error);
+ });
+ },
},
};
</script>
@@ -128,6 +189,7 @@ export default {
:has-next-page="pageInfo.hasNextPage"
:has-previous-page="pageInfo.hasPreviousPage"
:has-scoped-labels-feature="hasScopedLabelsFeature"
+ :initial-sort-by="sortKey"
:issuables="issues"
:issuables-loading="$apollo.queries.issues.loading"
namespace="dashboard"
@@ -137,11 +199,13 @@ export default {
:show-pagination-controls="showPaginationControls"
:sort-options="sortOptions"
:tabs="$options.IssuableListTabs"
+ :url-params="urlParams"
use-keyset-pagination
@click-tab="handleClickTab"
@dismiss-alert="handleDismissAlert"
@next-page="handleNextPage"
@previous-page="handlePreviousPage"
+ @sort="handleSort"
>
<template #nav-actions>
<gl-button :href="rssPath" icon="rss">
diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js
index ed11a600d4c..e3e5cc614cb 100644
--- a/app/assets/javascripts/issues/dashboard/index.js
+++ b/app/assets/javascripts/issues/dashboard/index.js
@@ -20,6 +20,7 @@ export function mountIssuesDashboardApp() {
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
hasScopedLabelsFeature,
+ initialSort,
isPublicVisibilityRestricted,
isSignedIn,
rssPath,
@@ -38,6 +39,7 @@ export function mountIssuesDashboardApp() {
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
+ initialSort,
isPublicVisibilityRestricted: parseBoolean(isPublicVisibilityRestricted),
isSignedIn: parseBoolean(isSignedIn),
rssPath,
diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
index 2e70fb1eade..6d0c7139068 100644
--- a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
@@ -4,6 +4,7 @@
query getDashboardIssues(
$hideUsers: Boolean = false
$isSignedIn: Boolean = false
+ $sort: IssueSort
$state: IssuableState
$afterCursor: String
$beforeCursor: String
@@ -11,6 +12,7 @@ query getDashboardIssues(
$lastPageSize: Int
) {
issues(
+ sort: $sort
state: $state
after: $afterCursor
before: $beforeCursor
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 6e0782ac866..d661ce67d88 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -23,6 +23,7 @@ import {
OPERATORS_IS_NOT,
OPERATORS_IS_NOT_OR,
OPTIONS_NONE_ANY,
+ TOKEN_TITLE_SEARCH_WITHIN,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_CONFIDENTIAL,
@@ -43,6 +44,7 @@ import {
TOKEN_TYPE_ORGANIZATION,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_SEARCH_WITHIN,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
@@ -305,6 +307,22 @@ export default {
const tokens = [
{
+ type: TOKEN_TYPE_SEARCH_WITHIN,
+ title: TOKEN_TITLE_SEARCH_WITHIN,
+ icon: 'search',
+ token: GlFilteredSearchToken,
+ unique: true,
+ operators: OPERATORS_IS,
+ options: [
+ { icon: 'title', value: 'TITLE', title: this.$options.i18n.titles },
+ {
+ icon: 'text-description',
+ value: 'DESCRIPTION',
+ title: this.$options.i18n.descriptions,
+ },
+ ],
+ },
+ {
type: TOKEN_TYPE_AUTHOR,
title: TOKEN_TITLE_AUTHOR,
icon: 'pencil',
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index dc8adf9473f..683a5955465 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -22,6 +22,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
+ TOKEN_TYPE_SEARCH_WITHIN,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
WORK_ITEM_TYPE_ENUM_INCIDENT,
@@ -111,6 +112,8 @@ export const i18n = {
rssLabel: __('Subscribe to RSS feed'),
searchPlaceholder: __('Search or filter results...'),
upvotes: __('Upvotes'),
+ titles: __('Titles'),
+ descriptions: __('Descriptions'),
};
export const urlSortParams = {
@@ -120,8 +123,8 @@ export const urlSortParams = {
[CREATED_DESC]: 'created_date',
[UPDATED_ASC]: 'updated_asc',
[UPDATED_DESC]: 'updated_desc',
- [CLOSED_AT_ASC]: 'closed_asc',
- [CLOSED_AT_DESC]: 'closed_desc',
+ [CLOSED_AT_ASC]: 'closed_at',
+ [CLOSED_AT_DESC]: 'closed_at_desc',
[MILESTONE_DUE_ASC]: 'milestone',
[MILESTONE_DUE_DESC]: 'milestone_due_desc',
[DUE_DATE_ASC]: 'due_date',
@@ -189,6 +192,19 @@ export const filters = {
},
},
},
+ [TOKEN_TYPE_SEARCH_WITHIN]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'in',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'in',
+ },
+ [OPERATOR_NOT]: {
+ [NORMAL_FILTER]: 'not[in]',
+ },
+ },
+ },
[TOKEN_TYPE_ASSIGNEE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'assigneeUsernames',
diff --git a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
index b447289b425..ee97fb6edca 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
@@ -10,6 +10,7 @@ query getIssues(
$search: String
$sort: IssueSort
$state: IssuableState
+ $in: [IssuableSearchableField!]
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
@@ -38,6 +39,7 @@ query getIssues(
search: $search
sort: $sort
state: $state
+ in: $in
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
@@ -72,6 +74,7 @@ query getIssues(
search: $search
sort: $sort
state: $state
+ in: $in
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index f9f4e981863..993b4c11c0e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -60,6 +60,7 @@ export const TOKEN_TITLE_SOURCE_BRANCH = __('Source Branch');
export const TOKEN_TITLE_STATUS = __('Status');
export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
+export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
export const TOKEN_TYPE_APPROVED_BY = 'approved-by';
export const TOKEN_TYPE_ASSIGNEE = 'assignee';
@@ -84,3 +85,5 @@ export const TOKEN_TYPE_STATUS = 'status';
export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
export const TOKEN_TYPE_WEIGHT = 'weight';
+
+export const TOKEN_TYPE_SEARCH_WITHIN = 'in';
diff --git a/app/components/diffs/stats_component.rb b/app/components/diffs/stats_component.rb
index dc42a849ccf..407c3ca4e58 100644
--- a/app/components/diffs/stats_component.rb
+++ b/app/components/diffs/stats_component.rb
@@ -14,14 +14,14 @@ module Diffs
def diff_files_data
diffs_map = @diff_files.map do |f|
{
- href: "##{helpers.hexdigest(f.file_path)}",
- title: f.new_path,
- name: f.file_path,
- path: diff_file_path_text(f),
- icon: diff_file_changed_icon(f),
- iconColor: diff_file_changed_icon_color(f).to_s,
- added: f.added_lines,
- removed: f.removed_lines
+ href: "##{helpers.hexdigest(f.file_path)}",
+ title: f.new_path,
+ name: f.file_path,
+ path: diff_file_path_text(f),
+ icon: diff_file_changed_icon(f),
+ iconColor: diff_file_changed_icon_color(f).to_s,
+ added: f.added_lines,
+ removed: f.removed_lines
}
end
diff --git a/app/controllers/admin/ci/variables_controller.rb b/app/controllers/admin/ci/variables_controller.rb
index 02d551115d0..cd9bf422eee 100644
--- a/app/controllers/admin/ci/variables_controller.rb
+++ b/app/controllers/admin/ci/variables_controller.rb
@@ -32,8 +32,8 @@ class Admin::Ci::VariablesController < Admin::ApplicationController
def render_instance_variables
render status: :ok,
json: {
- variables: Ci::InstanceVariableSerializer.new.represent(variables)
- }
+ variables: Ci::InstanceVariableSerializer.new.represent(variables)
+ }
end
def render_error(errors)
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index 41f95addc66..96fb73cedfe 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -59,11 +59,11 @@ class Admin::SystemInfoController < Admin::ApplicationController
begin
disk = Sys::Filesystem.stat(mount.mount_point)
@disks.push({
- bytes_total: disk.bytes_total,
- bytes_used: disk.bytes_used,
- disk_name: mount.name,
- mount_path: disk.path
- })
+ bytes_total: disk.bytes_total,
+ bytes_used: disk.bytes_used,
+ disk_name: mount.name,
+ mount_path: disk.path
+ })
rescue Sys::Filesystem::Error
end
end
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index 0a859bd3af9..e1967c50d70 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -8,9 +8,9 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_issues_tab", {
- issues: @milestone.sorted_issues(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables
- show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
- })
+ issues: @milestone.sorted_issues(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
+ })
end
end
end
@@ -20,9 +20,9 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
- merge_requests: @milestone.sorted_merge_requests(current_user).preload_milestoneish_associations, # rubocop:disable Gitlab/ModuleWithInstanceVariables
- show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
- })
+ merge_requests: @milestone.sorted_merge_requests(current_user).preload_milestoneish_associations, # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
+ })
end
end
end
@@ -32,8 +32,8 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_participants_tab", {
- users: @milestone.issue_participants_visible_by_user(current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- })
+ users: @milestone.issue_participants_visible_by_user(current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ })
end
end
end
@@ -46,10 +46,10 @@ module MilestoneActions
milestone_labels = @milestone.issue_labels_visible_by_user(current_user)
render json: tabs_json("shared/milestones/_labels_tab", {
- labels: milestone_labels.map do |label|
- label.present(issuable_subject: @milestone.resource_parent)
- end
- })
+ labels: milestone_labels.map do |label|
+ label.present(issuable_subject: @milestone.resource_parent)
+ end
+ })
end
end
end
diff --git a/app/controllers/concerns/render_service_results.rb b/app/controllers/concerns/render_service_results.rb
index 0149a71d9f5..83b880096be 100644
--- a/app/controllers/concerns/render_service_results.rb
+++ b/app/controllers/concerns/render_service_results.rb
@@ -5,25 +5,25 @@ module RenderServiceResults
def success_response(result)
render({
- status: result[:http_status],
- json: result[:body]
- })
+ status: result[:http_status],
+ json: result[:body]
+ })
end
def continue_polling_response
render({
- status: :no_content,
- json: {
- status: _('processing'),
- message: _('Not ready yet. Try again later.')
- }
- })
+ status: :no_content,
+ json: {
+ status: _('processing'),
+ message: _('Not ready yet. Try again later.')
+ }
+ })
end
def error_response(result)
render({
- status: result[:http_status] || :bad_request,
- json: { status: result[:status], message: result[:message] }
- })
+ status: result[:http_status] || :bad_request,
+ json: { status: result[:status], message: result[:message] }
+ })
end
end
diff --git a/app/controllers/concerns/sourcegraph_decorator.rb b/app/controllers/concerns/sourcegraph_decorator.rb
index 061990a4361..4aeace1ca67 100644
--- a/app/controllers/concerns/sourcegraph_decorator.rb
+++ b/app/controllers/concerns/sourcegraph_decorator.rb
@@ -22,8 +22,8 @@ module SourcegraphDecorator
return unless sourcegraph_enabled?
gon.push({
- sourcegraph: { url: Gitlab::CurrentSettings.sourcegraph_url }
- })
+ sourcegraph: { url: Gitlab::CurrentSettings.sourcegraph_url }
+ })
end
def sourcegraph_enabled?
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 9ef5cf0ed09..03b7cc9f892 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -186,9 +186,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def u2f_registrations
current_user.u2f_registrations.map do |u2f_registration|
{
- name: u2f_registration.name,
- created_at: u2f_registration.created_at,
- delete_path: profile_u2f_registration_path(u2f_registration)
+ name: u2f_registration.name,
+ created_at: u2f_registration.created_at,
+ delete_path: profile_u2f_registration_path(u2f_registration)
}
end
end
@@ -196,9 +196,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def webauthn_registrations
current_user.webauthn_registrations.map do |webauthn_registration|
{
- name: webauthn_registration.name,
- created_at: webauthn_registration.created_at,
- delete_path: profile_webauthn_registration_path(webauthn_registration)
+ name: webauthn_registration.name,
+ created_at: webauthn_registration.created_at,
+ delete_path: profile_webauthn_registration_path(webauthn_registration)
}
end
end
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 42bd87e1c01..dbbffc4c283 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -13,10 +13,10 @@ class Projects::BadgesController < Projects::ApplicationController
def pipeline
pipeline_status = Gitlab::Ci::Badge::Pipeline::Status
.new(project, params[:ref], opts: {
- ignore_skipped: params[:ignore_skipped],
- key_text: params[:key_text],
- key_width: params[:key_width]
- })
+ ignore_skipped: params[:ignore_skipped],
+ key_text: params[:key_text],
+ key_width: params[:key_width]
+ })
render_badge pipeline_status
end
@@ -24,13 +24,13 @@ class Projects::BadgesController < Projects::ApplicationController
def coverage
coverage_report = Gitlab::Ci::Badge::Coverage::Report
.new(project, params[:ref], opts: {
- job: params[:job],
- key_text: params[:key_text],
- key_width: params[:key_width],
- min_good: params[:min_good],
- min_acceptable: params[:min_acceptable],
- min_medium: params[:min_medium]
- })
+ job: params[:job],
+ key_text: params[:key_text],
+ key_width: params[:key_width],
+ min_good: params[:min_good],
+ min_acceptable: params[:min_acceptable],
+ min_medium: params[:min_medium]
+ })
render_badge coverage_report
end
@@ -38,10 +38,10 @@ class Projects::BadgesController < Projects::ApplicationController
def release
latest_release = Gitlab::Ci::Badge::Release::LatestRelease
.new(project, current_user, opts: {
- key_text: params[:key_text],
- key_width: params[:key_width],
- order_by: params[:order_by]
- })
+ key_text: params[:key_text],
+ key_width: params[:key_width],
+ order_by: params[:order_by]
+ })
render_badge latest_release
end
diff --git a/app/controllers/repositories/lfs_locks_api_controller.rb b/app/controllers/repositories/lfs_locks_api_controller.rb
index 0b765aa6931..ea858d63236 100644
--- a/app/controllers/repositories/lfs_locks_api_controller.rb
+++ b/app/controllers/repositories/lfs_locks_api_controller.rb
@@ -54,9 +54,9 @@ module Repositories
def error_payload(message, custom_attrs = {})
custom_attrs.merge({
- message: message,
- documentation_url: help_url
- })
+ message: message,
+ documentation_url: help_url
+ })
end
def split_by_owner(locks)
diff --git a/app/experiments/concerns/project_commit_count.rb b/app/experiments/concerns/project_commit_count.rb
index 706a1a24640..3f08538c21f 100644
--- a/app/experiments/concerns/project_commit_count.rb
+++ b/app/experiments/concerns/project_commit_count.rb
@@ -10,9 +10,9 @@ module ProjectCommitCount
return default_count unless root_ref
Gitlab::GitalyClient::CommitService.new(raw_repo).commit_count(root_ref, {
- all: true, # include all branches
- max_count: max_count # limit as an optimization
- })
+ all: true, # include all branches
+ max_count: max_count # limit as an optimization
+ })
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, exception_details)
diff --git a/app/graphql/mutations/clusters/agent_tokens/create.rb b/app/graphql/mutations/clusters/agent_tokens/create.rb
index a99a54fa5ed..c10e1633350 100644
--- a/app/graphql/mutations/clusters/agent_tokens/create.rb
+++ b/app/graphql/mutations/clusters/agent_tokens/create.rb
@@ -49,9 +49,9 @@ module Mutations
payload = result.payload
{
- secret: payload[:secret],
- token: payload[:token],
- errors: Array.wrap(result.message)
+ secret: payload[:secret],
+ token: payload[:token],
+ errors: Array.wrap(result.message)
}
end
diff --git a/app/graphql/mutations/notes/create/diff_note.rb b/app/graphql/mutations/notes/create/diff_note.rb
index 7b8c06fd104..df2bd55106e 100644
--- a/app/graphql/mutations/notes/create/diff_note.rb
+++ b/app/graphql/mutations/notes/create/diff_note.rb
@@ -31,10 +31,10 @@ module Mutations
def create_note_params(noteable, args)
super(noteable, args).merge({
- type: 'DiffNote',
- position: position(noteable, args),
- merge_request_diff_head_sha: args[:position][:head_sha]
- })
+ type: 'DiffNote',
+ position: position(noteable, args),
+ merge_request_diff_head_sha: args[:position][:head_sha]
+ })
end
def position(noteable, args)
diff --git a/app/graphql/mutations/notes/create/image_diff_note.rb b/app/graphql/mutations/notes/create/image_diff_note.rb
index d94fd4d6ff8..3de93e4f5c1 100644
--- a/app/graphql/mutations/notes/create/image_diff_note.rb
+++ b/app/graphql/mutations/notes/create/image_diff_note.rb
@@ -15,9 +15,9 @@ module Mutations
def create_note_params(noteable, args)
super(noteable, args).merge({
- type: 'DiffNote',
- position: position(noteable, args)
- })
+ type: 'DiffNote',
+ position: position(noteable, args)
+ })
end
def position(noteable, args)
diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb
index 4d6f056de09..9b105b7fe1c 100644
--- a/app/graphql/mutations/notes/create/note.rb
+++ b/app/graphql/mutations/notes/create/note.rb
@@ -31,9 +31,9 @@ module Mutations
end
super(noteable, args).merge({
- in_reply_to_discussion_id: discussion_id,
- merge_request_diff_head_sha: args[:merge_request_diff_head_sha]
- })
+ in_reply_to_discussion_id: discussion_id,
+ merge_request_diff_head_sha: args[:merge_request_diff_head_sha]
+ })
end
def authorize_discussion!(discussion)
diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb
index 20913a9e7da..f2f944860c2 100644
--- a/app/graphql/mutations/todos/restore_many.rb
+++ b/app/graphql/mutations/todos/restore_many.rb
@@ -23,9 +23,9 @@ module Mutations
updated_ids = restore(todos)
{
- updated_ids: updated_ids,
- todos: Todo.id_in(updated_ids),
- errors: errors_on_objects(todos)
+ updated_ids: updated_ids,
+ todos: Todo.id_in(updated_ids),
+ errors: errors_on_objects(todos)
}
end
diff --git a/app/graphql/resolvers/group_packages_resolver.rb b/app/graphql/resolvers/group_packages_resolver.rb
index e6a6abb39dd..ae578390fd5 100644
--- a/app/graphql/resolvers/group_packages_resolver.rb
+++ b/app/graphql/resolvers/group_packages_resolver.rb
@@ -13,9 +13,9 @@ module Resolvers
default_value: :created_desc
GROUP_SORT_TO_PARAMS_MAP = SORT_TO_PARAMS_MAP.merge({
- project_path_desc: { order_by: 'project_path', sort: 'desc' },
- project_path_asc: { order_by: 'project_path', sort: 'asc' }
- }).freeze
+ project_path_desc: { order_by: 'project_path', sort: 'desc' },
+ project_path_asc: { order_by: 'project_path', sort: 'asc' }
+ }).freeze
def resolve(sort:, **filters)
return unless packages_available?
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 9d404736dac..1d68dccc741 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -260,6 +260,7 @@ module IssuesHelper
{
calendar_path: url_for(safe_params.merge(calendar_url_options)),
empty_state_svg_path: image_path('illustrations/issue-dashboard_results-without-filter.svg'),
+ initial_sort: current_user&.user_preference&.issues_sort,
is_public_visibility_restricted:
Gitlab::CurrentSettings.restricted_visibility_levels&.include?(Gitlab::VisibilityLevel::PUBLIC).to_s,
is_signed_in: current_user.present?.to_s,
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index cbfcb1807f0..7a2a91aa0d2 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -57,6 +57,7 @@ module Integrations
label: Integration::SNOWPLOW_EVENT_LABEL,
property: key,
user: User.find(user_id),
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context],
**optional_arguments
)
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 7945e185e8c..45302a0bd09 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -404,6 +404,7 @@ module Integrations
label: Integration::SNOWPLOW_EVENT_LABEL,
property: key,
user: user,
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context],
**optional_arguments
)
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 5d66e7d2854..bb266d86cdb 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -265,10 +265,24 @@ class Project < ApplicationRecord
has_many :incident_management_issuable_escalation_statuses, through: :issues, inverse_of: :project, class_name: 'IncidentManagement::IssuableEscalationStatus'
has_many :incident_management_timeline_event_tags, inverse_of: :project, class_name: 'IncidentManagement::TimelineEventTag'
has_many :labels, class_name: 'ProjectLabel'
- has_many :integrations
has_many :events
has_many :milestones
+ has_many :integrations
+ has_many :alert_hooks_integrations, -> { alert_hooks }, class_name: 'Integration'
+ has_many :archive_trace_hooks_integrations, -> { archive_trace_hooks }, class_name: 'Integration'
+ has_many :confidential_issue_hooks_integrations, -> { confidential_issue_hooks }, class_name: 'Integration'
+ has_many :confidential_note_hooks_integrations, -> { confidential_note_hooks }, class_name: 'Integration'
+ has_many :deployment_hooks_integrations, -> { deployment_hooks }, class_name: 'Integration'
+ has_many :issue_hooks_integrations, -> { issue_hooks }, class_name: 'Integration'
+ has_many :job_hooks_integrations, -> { job_hooks }, class_name: 'Integration'
+ has_many :merge_request_hooks_integrations, -> { merge_request_hooks }, class_name: 'Integration'
+ has_many :note_hooks_integrations, -> { note_hooks }, class_name: 'Integration'
+ has_many :pipeline_hooks_integrations, -> { pipeline_hooks }, class_name: 'Integration'
+ has_many :push_hooks_integrations, -> { push_hooks }, class_name: 'Integration'
+ has_many :tag_push_hooks_integrations, -> { tag_push_hooks }, class_name: 'Integration'
+ has_many :wiki_page_hooks_integrations, -> { wiki_page_hooks }, class_name: 'Integration'
+
# Projects with a very large number of notes may time out destroying them
# through the foreign key. Additionally, the deprecated attachment uploader
# for notes requires us to use dependent: :destroy to avoid orphaning uploaded
@@ -1713,8 +1727,14 @@ class Project < ApplicationRecord
def execute_integrations(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
run_after_commit_or_now do
- integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
- integration.async_execute(data)
+ if use_integration_relations?
+ association("#{hooks_scope}_integrations").reader.each do |integration|
+ integration.async_execute(data)
+ end
+ else
+ integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
+ integration.async_execute(data)
+ end
end
end
end
@@ -3347,6 +3367,12 @@ class Project < ApplicationRecord
ProjectFeature::PRIVATE
end
end
+
+ def use_integration_relations?
+ strong_memoize(:use_integration_relations) do
+ Feature.enabled?(:cache_project_integrations, self)
+ end
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_export_job.rb b/app/models/project_export_job.rb
index 47be692d57a..d26ce5465cd 100644
--- a/app/models/project_export_job.rb
+++ b/app/models/project_export_job.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
class ProjectExportJob < ApplicationRecord
+ include EachBatch
+
+ EXPIRES_IN = 7.days
+
belongs_to :project
has_many :relation_exports, class_name: 'Projects::ImportExport::RelationExport'
@@ -13,6 +17,8 @@ class ProjectExportJob < ApplicationRecord
failed: 3
}.freeze
+ scope :prunable, -> { where("updated_at < ?", EXPIRES_IN.ago) }
+
state_machine :status, initial: :queued do
event :start do
transition [:queued] => :started
@@ -31,4 +37,12 @@ class ProjectExportJob < ApplicationRecord
state :finished, value: STATUS[:finished]
state :failed, value: STATUS[:failed]
end
+
+ class << self
+ def prune_expired_jobs
+ prunable.each_batch do |relation| # rubocop:disable Style/SymbolProc
+ relation.delete_all
+ end
+ end
+ end
end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index 41c924029d7..1ce866bd910 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -19,6 +19,14 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:deactivated) { @user&.deactivated? }
+ desc "User is bot"
+ with_options scope: :user, score: 0
+ condition(:bot) { @user&.bot? }
+
+ desc "User is alert bot"
+ with_options scope: :user, score: 0
+ condition(:alert_bot) { @user&.alert_bot? }
+
desc "User is support bot"
with_options scope: :user, score: 0
condition(:support_bot) { @user&.support_bot? }
@@ -50,9 +58,6 @@ class BasePolicy < DeclarativePolicy::Base
::Gitlab::ExternalAuthorization.perform_check?
end
- with_options scope: :user, score: 0
- condition(:alert_bot) { @user&.alert_bot? }
-
rule { external_authorization_enabled & ~can?(:read_all_resources) }.policy do
prevent :read_cross_project
end
@@ -68,8 +73,6 @@ class BasePolicy < DeclarativePolicy::Base
rule { default }.enable :read_cross_project
condition(:is_gitlab_com, score: 0, scope: :global) { ::Gitlab.com? }
-
- condition(:is_bot?) { @user&.bot? }
end
BasePolicy.prepend_mod_with('BasePolicy')
diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb
index 62840b0129f..32128d84d0b 100644
--- a/app/policies/merge_request_policy.rb
+++ b/app/policies/merge_request_policy.rb
@@ -18,7 +18,7 @@ class MergeRequestPolicy < IssuablePolicy
enable :approve_merge_request
end
- rule { can?(:approve_merge_request) & is_bot? }.policy do
+ rule { can?(:approve_merge_request) & bot }.policy do
enable :reset_merge_request_approvals
end
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index 4edb0f324dc..8750b80ccfd 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,8 +1,7 @@
.nav-block.activities
= render 'shared/event_filter'
.controls
- = link_to dashboard_projects_path(rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip', title: 'Subscribe' do
- = sprite_icon('rss', css_class: 'gl-icon')
+ = render Pajamas::ButtonComponent.new(href: dashboard_projects_path(rss_url_options), icon: 'rss', button_options: { title: _('Subscribe'), aria: { label: _('Subscribe') }, class: 'gl-display-none gl-sm-display-inline-flex' })
.content_list
.loading
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 9c492a0da34..a819a39af8f 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -9,7 +9,8 @@
- if current_user.can_create_project?
.page-title-controls
- = link_to _("New project"), new_project_path, class: "gl-button btn btn-confirm", data: { qa_selector: 'new_project_button' }
+ = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { qa_selector: 'new_project_button' } }) do
+ = _("New project")
.top-area
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index be2124fdd7e..5a798c249d1 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -4,7 +4,8 @@
- if current_user && current_user.snippets.any? || @snippets.any?
.page-title-controls
- if can?(current_user, :create_snippet)
- = link_to _("New snippet"), new_snippet_path, class: "gl-button btn btn-confirm", title: _("New snippet")
+ = render Pajamas::ButtonComponent.new(href: new_snippet_path, variant: :confirm, button_options: { title: _("New snippet") }) do
+ = _("New snippet")
.top-area
= gl_tabs_nav({ class: 'gl-border-0' }) do
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 17931152bd0..b3bda801fad 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -426,6 +426,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: cronjob:export_prune_project_export_jobs
+ :worker_name: Gitlab::Export::PruneProjectExportJobsWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:gitlab_service_ping
:worker_name: GitlabServicePingWorker
:feature_category: :service_ping
diff --git a/app/workers/container_registry/migration/enqueuer_worker.rb b/app/workers/container_registry/migration/enqueuer_worker.rb
index 1dd29eff86e..a4f5ac8eb7e 100644
--- a/app/workers/container_registry/migration/enqueuer_worker.rb
+++ b/app/workers/container_registry/migration/enqueuer_worker.rb
@@ -130,13 +130,13 @@ module ContainerRegistry
# this issue.
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87733 and
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90735 for details.
- ContainerRepository.ready_for_import.limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord
+ ContainerRepository.ready_for_import.ordered.limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord
end
end
def next_aborted_repository
strong_memoize(:next_aborted_repository) do
- ContainerRepository.with_migration_state('import_aborted').limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord
+ ContainerRepository.with_migration_state('import_aborted').ordered.limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord
end
end
diff --git a/app/workers/gitlab/export/prune_project_export_jobs_worker.rb b/app/workers/gitlab/export/prune_project_export_jobs_worker.rb
new file mode 100644
index 00000000000..9a3c0c80f85
--- /dev/null
+++ b/app/workers/gitlab/export/prune_project_export_jobs_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Export
+ class PruneProjectExportJobsWorker
+ include ApplicationWorker
+
+ # rubocop:disable Scalability/CronWorkerContext
+ # This worker updates several import states inline and does not schedule
+ # other jobs. So no context needed
+ include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
+ feature_category :importers
+ data_consistency :always
+ idempotent!
+
+ def perform
+ ProjectExportJob.prune_expired_jobs
+ end
+ end
+ end
+end
diff --git a/bin/audit-event-type b/bin/audit-event-type
index 8704dcfc0b0..fec34724c7c 100755
--- a/bin/audit-event-type
+++ b/bin/audit-event-type
@@ -36,7 +36,7 @@ class AuditEventTypeOptionParser
Options = Struct.new(
:name,
:description,
- :group,
+ :feature_category,
:milestone,
:saved_to_database,
:streamed,
@@ -71,9 +71,9 @@ class AuditEventTypeOptionParser
options.description = value
end
- opts.on('-g', '--group [string]', String,
-"Name of the group that introduced this audit event. For example, govern::compliance") do |value|
- options.group = value
+ opts.on('-c', '--feature-category [string]', String,
+"The feature category of this audit event. For example, compliance_management") do |value|
+ options.feature_category = value
end
opts.on('-M', '--milestone [string]', String,
@@ -145,16 +145,16 @@ class AuditEventTypeOptionParser
end
end
- def read_group
+ def read_feature_category
$stdout.puts
- $stdout.puts ">> Specify the group introducing the audit event type, like `govern::compliance`:"
+ $stdout.puts ">> Specify the feature category of this audit event, like `compliance_management`:"
loop do
- group = Readline.readline('?> ', false)&.strip
- group = nil if group.empty?
- return group unless group.nil?
+ feature_category = Readline.readline('?> ', false)&.strip
+ feature_category = nil if feature_category.empty?
+ return feature_category unless feature_category.nil?
- warn "group is a required field."
+ warn "feature_category is a required field."
end
end
@@ -231,7 +231,7 @@ class AuditEventTypeCreator
assert_existing_audit_event_type!
options.description ||= AuditEventTypeOptionParser.read_description
- options.group ||= AuditEventTypeOptionParser.read_group
+ options.feature_category ||= AuditEventTypeOptionParser.read_feature_category
options.milestone ||= AuditEventTypeOptionParser.read_milestone
options.saved_to_database = AuditEventTypeOptionParser.read_saved_to_database if options.saved_to_database.nil?
options.streamed = AuditEventTypeOptionParser.read_streamed if options.streamed.nil?
@@ -263,7 +263,7 @@ class AuditEventTypeCreator
{
'name' => options.name,
'description' => options.description,
- 'group' => options.group,
+ 'feature_category' => options.feature_category,
'milestone' => options.milestone,
'saved_to_database' => options.saved_to_database,
'streamed' => options.streamed,
diff --git a/config/audit_events/types/policy_project_updated.yml b/config/audit_events/types/policy_project_updated.yml
index 6fffc7f6b10..cb69c2ab796 100644
--- a/config/audit_events/types/policy_project_updated.yml
+++ b/config/audit_events/types/policy_project_updated.yml
@@ -3,6 +3,6 @@ description: "This event is triggered whenever the security policy project is up
introduced_by_issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/377877"
introduced_by_mr: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102154"
milestone: "15.6"
-group: "govern::security policies"
+feature_category: "security_policy_management"
saved_to_database: true
streamed: false
diff --git a/config/audit_events/types/type_schema.json b/config/audit_events/types/type_schema.json
index 0d5d79bc4c4..3921b36ba91 100644
--- a/config/audit_events/types/type_schema.json
+++ b/config/audit_events/types/type_schema.json
@@ -28,9 +28,9 @@
"https"
]
},
- "group": {
+ "feature_category": {
"type": "string",
- "description": "Name of the group that introduced this audit event. For example, manage::compliance"
+ "description": "The feature category of this audit event. For example, compliance_management"
},
"milestone": {
"type": "string",
@@ -48,7 +48,7 @@
},
"required": [
"description",
- "group",
+ "feature_category",
"introduced_by_issue",
"introduced_by_mr",
"milestone",
diff --git a/config/feature_flags/development/approval_rules_pagination.yml b/config/feature_flags/development/cache_project_integrations.yml
index 78d4ad37ced..3bb652d4b51 100644
--- a/config/feature_flags/development/approval_rules_pagination.yml
+++ b/config/feature_flags/development/cache_project_integrations.yml
@@ -1,8 +1,8 @@
---
-name: approval_rules_pagination
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91702
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366823
-milestone: '15.2'
+name: cache_project_integrations
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104062
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/384004
+milestone: '15.7'
type: development
-group: group::source code
-default_enabled: true
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/ops/purge_stale_security_findings.yml b/config/feature_flags/ops/purge_stale_security_findings.yml
deleted file mode 100644
index b540c8a1d60..00000000000
--- a/config/feature_flags/ops/purge_stale_security_findings.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: purge_stale_security_findings
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81423
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356464
-milestone: '14.9'
-type: ops
-group: group::threat insights
-default_enabled: true
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index c49fcfd5495..4d8d09313b6 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -526,6 +526,9 @@ Settings.cron_jobs['remove_unaccepted_member_invites_worker']['job_class'] = 'Re
Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '0 */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
+Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker']['cron'] ||= '30 3 * * */7'
+Settings.cron_jobs['gitlab_export_prune_project_export_jobs_worker']['job_class'] = 'Gitlab::Export::PruneProjectExportJobsWorker'
Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *'
Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsWorker'
diff --git a/danger/plugins/stable_branch.rb b/danger/plugins/stable_branch.rb
new file mode 100644
index 00000000000..81dae35ff24
--- /dev/null
+++ b/danger/plugins/stable_branch.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require_relative '../../tooling/danger/stable_branch'
+
+module Danger
+ class StableBranch < ::Danger::Plugin
+ include Tooling::Danger::StableBranch
+ end
+end
diff --git a/danger/stable_branch_patch/Dangerfile b/danger/stable_branch_patch/Dangerfile
new file mode 100644
index 00000000000..b3326e82020
--- /dev/null
+++ b/danger/stable_branch_patch/Dangerfile
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+stable_branch.check!
diff --git a/db/post_migrate/20221128120634_schedule_fixing_security_scan_statuses.rb b/db/post_migrate/20221128120634_schedule_fixing_security_scan_statuses.rb
new file mode 100644
index 00000000000..1cf4a33e09f
--- /dev/null
+++ b/db/post_migrate/20221128120634_schedule_fixing_security_scan_statuses.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+class ScheduleFixingSecurityScanStatuses < Gitlab::Database::Migration[2.0]
+ MIGRATION = 'FixSecurityScanStatuses'
+ TABLE_NAME = :security_scans
+ BATCH_COLUMN = :id
+ DELAY_INTERVAL = 2.minutes
+ BATCH_SIZE = 10_000
+ MAX_BATCH_SIZE = 50_000
+ SUB_BATCH_SIZE = 100
+
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ class SecurityScan < MigrationRecord
+ def self.start_migration_from
+ sort_order = Arel::Nodes::SqlLiteral.new("date(timezone('UTC'::text, created_at)) ASC, id ASC")
+
+ where("date(timezone('UTC'::text, created_at)) > ?", 90.days.ago).order(sort_order).first&.id
+ end
+ end
+
+ def up
+ # Only the SaaS application is affected
+ return unless Gitlab.dev_or_test_env? || Gitlab.com?
+
+ batch_min_value = SecurityScan.start_migration_from
+
+ return unless batch_min_value # It is possible that some users don't have corrupted records
+
+ queue_batched_background_migration(
+ MIGRATION,
+ TABLE_NAME,
+ BATCH_COLUMN,
+ job_interval: DELAY_INTERVAL,
+ batch_size: BATCH_SIZE,
+ max_batch_size: MAX_BATCH_SIZE,
+ sub_batch_size: SUB_BATCH_SIZE,
+ batch_min_value: batch_min_value
+ )
+ end
+
+ def down
+ delete_batched_background_migration(
+ MIGRATION,
+ TABLE_NAME,
+ BATCH_COLUMN,
+ []
+ )
+ end
+end
diff --git a/db/post_migrate/20221129124240_remove_flowdock_integration_records.rb b/db/post_migrate/20221129124240_remove_flowdock_integration_records.rb
new file mode 100644
index 00000000000..6390ed0d53b
--- /dev/null
+++ b/db/post_migrate/20221129124240_remove_flowdock_integration_records.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class RemoveFlowdockIntegrationRecords < Gitlab::Database::Migration[2.0]
+ disable_ddl_transaction!
+
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ class Integration < MigrationRecord
+ include EachBatch
+
+ self.table_name = 'integrations'
+ end
+
+ def up
+ Integration.each_batch(of: 1000, column: :id) do |relation|
+ relation.delete_by(type_new: 'Integrations::Flowdock')
+ end
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/schema_migrations/20221128120634 b/db/schema_migrations/20221128120634
new file mode 100644
index 00000000000..4a2fa52d675
--- /dev/null
+++ b/db/schema_migrations/20221128120634
@@ -0,0 +1 @@
+011a7add2949c39e642da2f9d7908f6e2a118c91f2e334e0eee623711576c3cb \ No newline at end of file
diff --git a/db/schema_migrations/20221129124240 b/db/schema_migrations/20221129124240
new file mode 100644
index 00000000000..9b0199dc748
--- /dev/null
+++ b/db/schema_migrations/20221129124240
@@ -0,0 +1 @@
+ae20537326115d37db8beb3432ffd3ace447b39a75906535d319da4db1fcb1b2 \ No newline at end of file
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
index 4d52bf36f20..bc91f2752c6 100644
--- a/doc/api/merge_request_approvals.md
+++ b/doc/api/merge_request_approvals.md
@@ -84,6 +84,7 @@ Supported attributes:
> - Moved to GitLab Premium in 13.9.
> - Pagination support [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31011) in GitLab 15.3 [with a flag](../administration/feature_flags.md) named `approval_rules_pagination`. Enabled by default.
> - `applies_to_all_protected_branches` property was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335316) in GitLab 15.3.
+> - Pagination support [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/366823) in GitLab 15.7. Feature flag `approval_rules_pagination` removed.
You can request information about a project's approval rules using the following endpoint:
@@ -704,6 +705,7 @@ Supported attributes:
> - Moved to GitLab Premium in 13.9.
> - Pagination support [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31011) in GitLab 15.3 [with a flag](../administration/feature_flags.md) named `approval_rules_pagination`. Enabled by default.
+> - Pagination support [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/366823) in GitLab 15.7. Feature flag `approval_rules_pagination` removed.
You can request information about a merge request's approval rules using the following endpoint:
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 93ff10a4132..e2340e39903 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -221,6 +221,9 @@ See the [test engineering process](https://about.gitlab.com/handbook/engineering
1. You have confirmed that if this MR contains changes to processing or storing of credentials or tokens, authorization, and authentication methods, or other items described in [the security review guidelines](https://about.gitlab.com/handbook/security/#when-to-request-a-security-review), you have added the `~security` label and you have `@`-mentioned `@gitlab-com/gl-security/appsec`.
1. You have reviewed the documentation regarding [internal application security reviews](https://about.gitlab.com/handbook/security/#internal-application-security-reviews) for **when** and **how** to request a security review and requested a security review if this is warranted for this change.
+1. If there are security scan results that are blocking the MR (due to the [scan result policies](https://gitlab.com/gitlab-com/gl-security/security-policies)):
+ - For true positive findings, they should be corrected before the merge request is merged. This will remove the AppSec approval required by the scan result policy.
+ - For false positive findings, something that should be discussed for risk acceptance, or anything questionable, please ping `@gitlab-com/gl-security/appsec`.
##### Deployment
diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb
index 5dea41bfdb7..35fdcfe3ab0 100644
--- a/lib/api/merge_request_approvals.rb
+++ b/lib/api/merge_request_approvals.rb
@@ -88,6 +88,7 @@ module API
present_approval(merge_request)
end
+
desc 'Remove all merge request approvals' do
detail 'Clear all approvals of merge request. This feature was added in GitLab 15.4'
failure [
diff --git a/lib/gitlab/audit/type/shared.rb b/lib/gitlab/audit/type/shared.rb
index 999b7de13e2..1e3e26d3735 100644
--- a/lib/gitlab/audit/type/shared.rb
+++ b/lib/gitlab/audit/type/shared.rb
@@ -14,7 +14,7 @@ module Gitlab
description
introduced_by_issue
introduced_by_mr
- group
+ feature_category
milestone
saved_to_database
streamed
diff --git a/lib/gitlab/background_migration/fix_security_scan_statuses.rb b/lib/gitlab/background_migration/fix_security_scan_statuses.rb
new file mode 100644
index 00000000000..b60e739f870
--- /dev/null
+++ b/lib/gitlab/background_migration/fix_security_scan_statuses.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Fixes the `status` attribute of `security_scans` records
+ class FixSecurityScanStatuses < BatchedMigrationJob
+ def perform
+ # no-op. The logic is defined in EE module.
+ end
+ end
+ end
+end
+
+::Gitlab::BackgroundMigration::FixSecurityScanStatuses.prepend_mod
diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml
index 2d9c8d1108e..cc69ed55744 100644
--- a/lib/gitlab/import_export/project/import_export.yml
+++ b/lib/gitlab/import_export/project/import_export.yml
@@ -763,6 +763,19 @@ excluded_attributes:
- :import_type
- :import_source
- :integrations
+ - :push_hooks_integrations
+ - :tag_push_hooks_integrations
+ - :issue_hooks_integrations
+ - :confidential_issue_hooks_integrations
+ - :merge_request_hooks_integrations
+ - :note_hooks_integrations
+ - :confidential_note_hooks_integrations
+ - :job_hooks_integrations
+ - :archive_trace_hooks_integrations
+ - :pipeline_hooks_integrations
+ - :wiki_page_hooks_integrations
+ - :deployment_hooks_integrations
+ - :alert_hooks_integrations
- :mirror
- :runners_token
- :runners_token_encrypted
@@ -1209,7 +1222,9 @@ ee:
- :description
iterations_cadence:
- :title
-
+ excluded_attributes:
+ project:
+ - :vulnerability_hooks_integrations
preloads:
issues:
epic:
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1b17569ef84..a879bb8b976 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -13803,6 +13803,9 @@ msgstr ""
msgid "Description:"
msgstr ""
+msgid "Descriptions"
+msgstr ""
+
msgid "Descriptive label"
msgstr ""
@@ -36199,6 +36202,9 @@ msgstr ""
msgid "Search GitLab"
msgstr ""
+msgid "Search Within"
+msgstr ""
+
msgid "Search a group"
msgstr ""
@@ -42779,6 +42785,9 @@ msgstr ""
msgid "Title:"
msgstr ""
+msgid "Titles"
+msgstr ""
+
msgid "Titles and Descriptions"
msgstr ""
diff --git a/qa/qa/support/loglinking.rb b/qa/qa/support/loglinking.rb
index 74d72ca3a3e..5a1aad3c7eb 100644
--- a/qa/qa/support/loglinking.rb
+++ b/qa/qa/support/loglinking.rb
@@ -22,8 +22,8 @@ module QA
pre: 'https://nonprod-log.gitlab.net/'
}.freeze
KIBANA_INDICES = {
- staging: 'pubsub-rails-inf-gstg',
- production: 'pubsub-rails-inf-gprd',
+ staging: 'ed942d00-5186-11ea-ad8a-f3610a492295',
+ production: '7092c4e2-4eb5-46f2-8305-a7da2edad090',
pre: 'pubsub-rails-inf-pre'
}.freeze
@@ -64,7 +64,7 @@ module QA
end
def get_kibana_url(base_url, index, correlation_id)
- "#{base_url}app/discover#/?_a=%28index:#{index}%2Cquery%3A%28language%3Akuery%2C" \
+ "#{base_url}app/discover#/?_a=%28index:%27#{index}%27%2Cquery%3A%28language%3Akuery%2C" \
"query%3A%27json.correlation_id%20%3A%20#{correlation_id}%27%29%29" \
"&_g=%28time%3A%28from%3A%27#{start_time}%27%2Cto%3A%27#{end_time}%27%29%29"
end
diff --git a/qa/spec/resource/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb
index cf47574fcce..76cc8e0e303 100644
--- a/qa/spec/resource/api_fabricator_spec.rb
+++ b/qa/spec/resource/api_fabricator_spec.rb
@@ -157,7 +157,7 @@ RSpec.describe QA::Resource::ApiFabricator do
Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.
Correlation Id: foobar
Sentry Url: https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg&query=correlation_id%3A%22foobar%22
- Kibana Url: https://nonprod-log.gitlab.net/app/discover#/?_a=%28index:pubsub-rails-inf-gstg%2Cquery%3A%28language%3Akuery%2Cquery%3A%27json.correlation_id%20%3A%20foobar%27%29%29&_g=%28time%3A%28from%3A%272022-11-13T00:00:00.000Z%27%2Cto%3A%272022-11-14T00:00:00.000Z%27%29%29
+ Kibana Url: https://nonprod-log.gitlab.net/app/discover#/?_a=%28index:%27ed942d00-5186-11ea-ad8a-f3610a492295%27%2Cquery%3A%28language%3Akuery%2Cquery%3A%27json.correlation_id%20%3A%20foobar%27%29%29&_g=%28time%3A%28from%3A%272022-11-13T00:00:00.000Z%27%2Cto%3A%272022-11-14T00:00:00.000Z%27%29%29
ERROR
end
end
diff --git a/qa/spec/support/loglinking_spec.rb b/qa/spec/support/loglinking_spec.rb
index 24b157ae9c3..3955d266ef6 100644
--- a/qa/spec/support/loglinking_spec.rb
+++ b/qa/spec/support/loglinking_spec.rb
@@ -37,7 +37,7 @@ RSpec.describe QA::Support::Loglinking do
expect(QA::Support::Loglinking.failure_metadata('foo123')).to eql(<<~ERROR.chomp)
Correlation Id: foo123
- Kibana Url: https://kibana.address/app/discover#/?_a=%28index:pubsub-rails-inf-foo%2Cquery%3A%28language%3Akuery%2Cquery%3A%27json.correlation_id%20%3A%20foo123%27%29%29&_g=%28time%3A%28from%3A%272022-11-13T00:00:00.000Z%27%2Cto%3A%272022-11-14T00:00:00.000Z%27%29%29
+ Kibana Url: https://kibana.address/app/discover#/?_a=%28index:%27pubsub-rails-inf-foo%27%2Cquery%3A%28language%3Akuery%2Cquery%3A%27json.correlation_id%20%3A%20foo123%27%29%29&_g=%28time%3A%28from%3A%272022-11-13T00:00:00.000Z%27%2Cto%3A%272022-11-14T00:00:00.000Z%27%29%29
ERROR
end
end
@@ -93,9 +93,9 @@ RSpec.describe QA::Support::Loglinking do
describe '.get_kibana_index' do
let(:index_hash) do
{
- :staging => 'pubsub-rails-inf-gstg',
+ :staging => 'ed942d00-5186-11ea-ad8a-f3610a492295',
:staging_ref => nil,
- :production => 'pubsub-rails-inf-gprd',
+ :production => '7092c4e2-4eb5-46f2-8305-a7da2edad090',
:pre => 'pubsub-rails-inf-pre',
:foo => nil,
nil => nil
diff --git a/spec/bin/audit_event_type_spec.rb b/spec/bin/audit_event_type_spec.rb
index dbf22898525..e23d365f68f 100644
--- a/spec/bin/audit_event_type_spec.rb
+++ b/spec/bin/audit_event_type_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe 'bin/audit-event-type' do
using RSpec::Parameterized::TableSyntax
describe AuditEventTypeCreator do
- let(:argv) { %w[test_audit_event -d test -g govern::compliance -s -t -i https://url -m http://url] }
+ let(:argv) { %w[test_audit_event -d test -c compliance_management -s -t -i https://url -m http://url] }
let(:options) { AuditEventTypeOptionParser.parse(argv) }
let(:creator) { described_class.new(options) }
let(:existing_audit_event_types) do
@@ -71,8 +71,8 @@ RSpec.describe 'bin/audit-event-type' do
:force | %w[foo --force] | true
:description | %w[foo -d desc] | 'desc'
:description | %w[foo --description desc] | 'desc'
- :group | %w[foo -g govern::compliance] | 'govern::compliance'
- :group | %w[foo --group govern::compliance] | 'govern::compliance'
+ :feature_category | %w[foo -c audit_events] | 'audit_events'
+ :feature_category | %w[foo --feature-category audit_events] | 'audit_events'
:milestone | %w[foo -M 15.6] | '15.6'
:milestone | %w[foo --milestone 15.6] | '15.6'
:saved_to_database | %w[foo -s] | true
@@ -141,27 +141,27 @@ RSpec.describe 'bin/audit-event-type' do
end
end
- describe '.read_group' do
- let(:group) { 'govern::compliance' }
+ describe '.read_feature_category' do
+ let(:feature_category) { 'compliance_management' }
- it 'reads group from stdin' do
- expect(Readline).to receive(:readline).and_return(group)
+ it 'reads feature_category from stdin' do
+ expect(Readline).to receive(:readline).and_return(feature_category)
expect do
- expect(described_class.read_group).to eq('govern::compliance')
- end.to output(/Specify the group introducing the audit event type, like `govern::compliance`:/).to_stdout
+ expect(described_class.read_feature_category).to eq('compliance_management')
+ end.to output(/Specify the feature category of this audit event, like `compliance_management`:/).to_stdout
end
- context 'when group is empty' do
- let(:group) { '' }
+ context 'when feature category is empty' do
+ let(:feature_category) { '' }
it 'shows error message and retries' do
- expect(Readline).to receive(:readline).and_return(group)
+ expect(Readline).to receive(:readline).and_return(feature_category)
expect(Readline).to receive(:readline).and_raise('EOF')
expect do
- expect { described_class.read_group }.to raise_error(/EOF/)
- end.to output(/Specify the group introducing the audit event type, like `govern::compliance`:/)
- .to_stdout.and output(/group is a required field/).to_stderr
+ expect { described_class.read_feature_category }.to raise_error(/EOF/)
+ end.to output(/Specify the feature category of this audit event, like `compliance_management`:/)
+ .to_stdout.and output(/feature_category is a required field/).to_stderr
end
end
end
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 6146e649d70..9bede49268e 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -237,7 +237,8 @@ RSpec.describe 'Database schema' do
"Packages::Composer::Metadatum" => %w[composer_json],
"RawUsageData" => %w[payload], # Usage data payload changes often, we cannot use one schema
"Releases::Evidence" => %w[summary],
- "Vulnerabilities::Finding::Evidence" => %w[data] # Validation work in progress
+ "Vulnerabilities::Finding::Evidence" => %w[data], # Validation work in progress
+ "EE::Gitlab::BackgroundMigration::FixSecurityScanStatuses::SecurityScan" => %w[info] # This is a migration model
}.freeze
# We are skipping GEO models for now as it adds up complexity
diff --git a/spec/factories/projects/import_export/relation_export_upload.rb b/spec/factories/projects/import_export/relation_export_upload.rb
index eaa57d6ee59..4bd6a586720 100644
--- a/spec/factories/projects/import_export/relation_export_upload.rb
+++ b/spec/factories/projects/import_export/relation_export_upload.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
- factory :project_relation_export_upload, class: 'Projects::ImportExport::RelationExportUpload' do
+ factory :relation_export_upload, class: 'Projects::ImportExport::RelationExportUpload' do
relation_export factory: :project_relation_export
export_file { fixture_file_upload("spec/fixtures/gitlab/import_export/labels.tar.gz") }
end
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index 3195d5ff0a1..96b8daa22d8 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -7,10 +7,17 @@ import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import {
+ setSortPreferenceMutationResponse,
+ setSortPreferenceMutationResponseWithErrors,
+} from 'jest/issues/list/mock_data';
import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
-import { i18n } from '~/issues/list/constants';
+import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants';
+import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
+import { getSortKey, getSortOptions } from '~/issues/list/utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
@@ -31,6 +38,7 @@ describe('IssuesDashboardApp component', () => {
hasIssuableHealthStatusFeature: true,
hasIssueWeightsFeature: true,
hasScopedLabelsFeature: true,
+ initialSort: CREATED_DESC,
isPublicVisibilityRestricted: false,
isSignedIn: true,
rssPath: 'rss/path',
@@ -54,11 +62,19 @@ describe('IssuesDashboardApp component', () => {
wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.rssButtonText });
const mountComponent = ({
+ provide = {},
issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse),
+ sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
} = {}) => {
wrapper = mountExtended(IssuesDashboardApp, {
- apolloProvider: createMockApollo([[getIssuesQuery, issuesQueryHandler]]),
- provide: defaultProvide,
+ apolloProvider: createMockApollo([
+ [getIssuesQuery, issuesQueryHandler],
+ [setSortPreferenceMutation, sortPreferenceMutationResponse],
+ ]),
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
});
};
@@ -71,11 +87,23 @@ describe('IssuesDashboardApp component', () => {
hasNextPage: true,
hasPreviousPage: false,
hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
+ initialSortBy: CREATED_DESC,
+ issuables: issuesQueryResponse.data.issues.nodes,
+ issuablesLoading: false,
namespace: 'dashboard',
recentSearchesStorageKey: 'issues',
searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
showPaginationControls: true,
+ sortOptions: getSortOptions({
+ hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
+ }),
tabs: IssuesDashboardApp.IssuableListTabs,
+ urlParams: {
+ sort: urlSortParams[CREATED_DESC],
+ state: IssuableStates.Opened,
+ },
useKeysetPagination: true,
});
});
@@ -118,6 +146,51 @@ describe('IssuesDashboardApp component', () => {
});
});
+ describe('initial url params', () => {
+ describe('sort', () => {
+ describe('when initial sort value uses old enum values', () => {
+ const oldEnumSortValues = Object.values(urlSortParams);
+
+ it.each(oldEnumSortValues)('initial sort is set with value %s', (sort) => {
+ mountComponent({ provide: { initialSort: sort } });
+
+ expect(findIssuableList().props('initialSortBy')).toBe(getSortKey(sort));
+ });
+ });
+
+ describe('when initial sort value uses new GraphQL enum values', () => {
+ const graphQLEnumSortValues = Object.keys(urlSortParams);
+
+ it.each(graphQLEnumSortValues)('initial sort is set with value %s', (sort) => {
+ mountComponent({ provide: { initialSort: sort.toLowerCase() } });
+
+ expect(findIssuableList().props('initialSortBy')).toBe(sort);
+ });
+ });
+
+ describe('when initial sort value is invalid', () => {
+ it.each(['', 'asdf', null, undefined])(
+ 'initial sort is set to value CREATED_DESC',
+ (sort) => {
+ mountComponent({ provide: { initialSort: sort } });
+
+ expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC);
+ },
+ );
+ });
+ });
+
+ describe('state', () => {
+ it('is set from the url params', () => {
+ const initialState = IssuableStates.All;
+ setWindowLocation(`?state=${initialState}`);
+ mountComponent();
+
+ expect(findIssuableList().props('currentTab')).toBe(initialState);
+ });
+ });
+ });
+
describe('when there is an error fetching issues', () => {
beforeEach(() => {
mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
@@ -148,6 +221,12 @@ describe('IssuesDashboardApp component', () => {
it('updates ui to the new tab', () => {
expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
});
+
+ it('updates url to the new tab', () => {
+ expect(findIssuableList().props('urlParams')).toMatchObject({
+ state: IssuableStates.Closed,
+ });
+ });
});
describe.each(['next-page', 'previous-page'])(
@@ -164,5 +243,63 @@ describe('IssuesDashboardApp component', () => {
});
},
);
+
+ describe('when "sort" event is emitted by IssuableList', () => {
+ it.each(Object.keys(urlSortParams))(
+ 'updates to the new sort when payload is `%s`',
+ async (sortKey) => {
+ // Ensure initial sort key is different so we can trigger an update when emitting a sort key
+ if (sortKey === CREATED_DESC) {
+ mountComponent({ provide: { initialSort: UPDATED_DESC } });
+ } else {
+ mountComponent();
+ }
+
+ findIssuableList().vm.$emit('sort', sortKey);
+ await nextTick();
+
+ expect(findIssuableList().props('urlParams')).toMatchObject({
+ sort: urlSortParams[sortKey],
+ });
+ },
+ );
+
+ describe('when user is signed in', () => {
+ it('calls mutation to save sort preference', () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ mountComponent({ sortPreferenceMutationResponse: mutationMock });
+
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
+
+ expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: UPDATED_DESC } });
+ });
+
+ it('captures error when mutation response has errors', async () => {
+ const mutationMock = jest
+ .fn()
+ .mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
+ mountComponent({ sortPreferenceMutationResponse: mutationMock });
+
+ findIssuableList().vm.$emit('sort', UPDATED_DESC);
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
+ });
+ });
+
+ describe('when user is signed out', () => {
+ it('does not call mutation to save sort preference', () => {
+ const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
+ mountComponent({
+ provide: { isSignedIn: false },
+ sortPreferenceMutationResponse: mutationMock,
+ });
+
+ findIssuableList().vm.$emit('sort', CREATED_DESC);
+
+ expect(mutationMock).not.toHaveBeenCalled();
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index b820b7d02bc..d404225333a 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -61,6 +61,7 @@ import {
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_ORGANIZATION,
TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
@@ -597,6 +598,7 @@ describe('CE IssuesListApp component', () => {
{ type: TOKEN_TYPE_MY_REACTION },
{ type: TOKEN_TYPE_ORGANIZATION },
{ type: TOKEN_TYPE_RELEASE },
+ { type: TOKEN_TYPE_SEARCH_WITHIN },
{ type: TOKEN_TYPE_TYPE },
]);
});
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 83b863eb7e3..ed363268cdf 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -3,9 +3,8 @@
require 'spec_helper'
RSpec.describe IssuesHelper do
- let(:project) { create(:project) }
- let(:issue) { create(:issue, project: project) }
- let(:ext_project) { create(:project, :with_redmine_integration) }
+ let_it_be(:project) { create(:project) }
+ let_it_be_with_reload(:issue) { create(:issue, project: project) }
describe '#work_item_type_icon' do
it 'returns icon of all standard base types' do
@@ -392,6 +391,7 @@ RSpec.describe IssuesHelper do
expected = {
calendar_path: '#',
empty_state_svg_path: '#',
+ initial_sort: current_user&.user_preference&.issues_sort,
is_public_visibility_restricted: Gitlab::CurrentSettings.restricted_visibility_levels ? 'false' : '',
is_signed_in: current_user.present?.to_s,
rss_path: '#'
diff --git a/spec/lib/gitlab/audit/type/definition_spec.rb b/spec/lib/gitlab/audit/type/definition_spec.rb
index bd3b90f12f0..3d11edf84f7 100644
--- a/spec/lib/gitlab/audit/type/definition_spec.rb
+++ b/spec/lib/gitlab/audit/type/definition_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe Gitlab::Audit::Type::Definition do
description: 'Group deploy token is deleted',
introduced_by_issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/1',
introduced_by_mr: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1',
- group: 'govern::compliance',
+ feature_category: 'continuous_delivery',
milestone: '15.4',
saved_to_database: true,
streamed: true }
@@ -42,7 +42,7 @@ RSpec.describe Gitlab::Audit::Type::Definition do
:description | nil | %r{property '/description' is not of type: string}
:introduced_by_issue | nil | %r{property '/introduced_by_issue' is not of type: string}
:introduced_by_mr | nil | %r{property '/introduced_by_mr' is not of type: string}
- :group | nil | %r{property '/group' is not of type: string}
+ :feature_category | nil | %r{property '/feature_category' is not of type: string}
:milestone | nil | %r{property '/milestone' is not of type: string}
end
# rubocop:enable Layout/LineLength
@@ -109,7 +109,7 @@ RSpec.describe Gitlab::Audit::Type::Definition do
expect(audit_event_type_definition.name).to eq "group_deploy_token_destroyed"
expect(audit_event_type_definition.description).to eq "Group deploy token is deleted"
- expect(audit_event_type_definition.group).to eq "govern::compliance"
+ expect(audit_event_type_definition.feature_category).to eq "continuous_delivery"
expect(audit_event_type_definition.milestone).to eq "15.4"
expect(audit_event_type_definition.saved_to_database).to be true
expect(audit_event_type_definition.streamed).to be true
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index b9867f794dc..b34399d20f1 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -409,6 +409,20 @@ project:
- boards
- last_event
- integrations
+- push_hooks_integrations
+- tag_push_hooks_integrations
+- issue_hooks_integrations
+- confidential_issue_hooks_integrations
+- merge_request_hooks_integrations
+- note_hooks_integrations
+- confidential_note_hooks_integrations
+- job_hooks_integrations
+- archive_trace_hooks_integrations
+- pipeline_hooks_integrations
+- wiki_page_hooks_integrations
+- deployment_hooks_integrations
+- alert_hooks_integrations
+- vulnerability_hooks_integrations
- campfire_integration
- confluence_integration
- datadog_integration
diff --git a/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb b/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb
index a781139acab..d70e89c6856 100644
--- a/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb
+++ b/spec/lib/gitlab/import_export/project/exported_relations_merger_spec.rb
@@ -8,17 +8,17 @@ RSpec.describe Gitlab::ImportExport::Project::ExportedRelationsMerger do
let(:shared) { Gitlab::ImportExport::Shared.new(export_job.project) }
before do
- create(:project_relation_export_upload,
+ create(:relation_export_upload,
relation_export: create(:project_relation_export, relation: 'project', project_export_job: export_job),
export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/project.tar.gz")
)
- create(:project_relation_export_upload,
+ create(:relation_export_upload,
relation_export: create(:project_relation_export, relation: 'labels', project_export_job: export_job),
export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/labels.tar.gz")
)
- create(:project_relation_export_upload,
+ create(:relation_export_upload,
relation_export: create(:project_relation_export, relation: 'uploads', project_export_job: export_job),
export_file: fixture_file_upload("spec/fixtures/gitlab/import_export/uploads.tar.gz")
)
diff --git a/spec/migrations/remove_flowdock_integration_records_spec.rb b/spec/migrations/remove_flowdock_integration_records_spec.rb
new file mode 100644
index 00000000000..3f57515d18b
--- /dev/null
+++ b/spec/migrations/remove_flowdock_integration_records_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db/post_migrate/20221129124240_remove_flowdock_integration_records.rb')
+
+RSpec.describe RemoveFlowdockIntegrationRecords, feature_category: :integrations do
+ let(:integrations) { table(:integrations) }
+
+ before do
+ integrations.create!(type_new: 'Integrations::Flowdock')
+ integrations.create!(type_new: 'SomeOtherType')
+ end
+
+ it 'removes integrations records of type_new Integrations::Flowdock' do
+ expect(integrations.count).to eq(2)
+
+ migrate!
+
+ expect(integrations.count).to eq(1)
+ expect(integrations.first.type_new).to eq('SomeOtherType')
+ expect(integrations.where(type_new: 'Integrations::Flowdock')).to be_empty
+ end
+end
diff --git a/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
new file mode 100644
index 00000000000..e958593dc19
--- /dev/null
+++ b/spec/migrations/schedule_fixing_security_scan_statuses_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe ScheduleFixingSecurityScanStatuses, feature_category: :vulnerability_management do
+ let_it_be(:namespaces) { table(:namespaces) }
+ let_it_be(:projects) { table(:projects) }
+ let_it_be(:pipelines) { table(:ci_pipelines) }
+ let_it_be(:builds) { table(:ci_builds) }
+ let_it_be(:security_scans) { table(:security_scans) }
+
+ let_it_be(:namespace) { namespaces.create!(name: "foo", path: "bar") }
+ let_it_be(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
+ let_it_be(:pipeline) do
+ pipelines.create!(project_id: project.id, ref: 'master', sha: 'adf43c3a', status: 'success', partition_id: 1)
+ end
+
+ let_it_be(:ci_build) { builds.create!(commit_id: pipeline.id, retried: false, type: 'Ci::Build', partition_id: 1) }
+
+ let!(:security_scan_1) { security_scans.create!(build_id: ci_build.id, scan_type: 1, created_at: 91.days.ago) }
+ let!(:security_scan_2) { security_scans.create!(build_id: ci_build.id, scan_type: 2) }
+
+ let(:com?) { false }
+ let(:dev_or_test_env?) { false }
+ let(:migration) { described_class::MIGRATION }
+
+ before do
+ allow(::Gitlab).to receive(:com?).and_return(com?)
+ allow(::Gitlab).to receive(:dev_or_test_env?).and_return(dev_or_test_env?)
+
+ migrate!
+ end
+
+ describe '#up' do
+ shared_examples_for 'scheduler for fixing the security scans status' do
+ it 'schedules background job' do
+ expect(migration).to have_scheduled_batched_migration(
+ table_name: :security_scans,
+ column_name: :id,
+ interval: 2.minutes,
+ batch_size: 10_000,
+ max_batch_size: 50_000,
+ sub_batch_size: 100,
+ batch_min_value: security_scan_2.id
+ )
+ end
+ end
+
+ context 'when the migration does not run on GitLab.com or development environment' do
+ it 'does not schedule the migration' do
+ expect('FixSecurityScanStatuses').not_to have_scheduled_batched_migration
+ end
+ end
+
+ context 'when the migration runs on GitLab.com' do
+ let(:com?) { true }
+
+ it_behaves_like 'scheduler for fixing the security scans status'
+ end
+
+ context 'when the migration runs on dev environment' do
+ let(:dev_or_test_env?) { true }
+
+ it_behaves_like 'scheduler for fixing the security scans status'
+ end
+ end
+
+ describe '#down' do
+ it 'deletes all batched migration records' do
+ schema_migrate_down!
+
+ expect(migration).not_to have_scheduled_batched_migration
+ end
+ end
+end
diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb
index aa31e32036f..a4ccae459cf 100644
--- a/spec/models/integrations/jira_spec.rb
+++ b/spec/models/integrations/jira_spec.rb
@@ -587,7 +587,7 @@ RSpec.describe Integrations::Jira do
close_issue
end
- it_behaves_like 'Snowplow event tracking' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
subject { close_issue }
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
@@ -943,7 +943,7 @@ RSpec.describe Integrations::Jira do
subject
end
- it_behaves_like 'Snowplow event tracking' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { 'Integrations::Jira' }
let(:action) { 'perform_integrations_action' }
diff --git a/spec/models/project_export_job_spec.rb b/spec/models/project_export_job_spec.rb
index 653d4d2df27..01b0aaff0ff 100644
--- a/spec/models/project_export_job_spec.rb
+++ b/spec/models/project_export_job_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectExportJob, type: :model do
+RSpec.describe ProjectExportJob, feature_category: :importers, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:relation_exports) }
@@ -13,4 +13,54 @@ RSpec.describe ProjectExportJob, type: :model do
it { is_expected.to validate_presence_of(:jid) }
it { is_expected.to validate_presence_of(:status) }
end
+
+ context 'when pruning expired jobs' do
+ let_it_be(:old_job_1) { create(:project_export_job, updated_at: 37.months.ago) }
+ let_it_be(:old_job_2) { create(:project_export_job, updated_at: 12.months.ago) }
+ let_it_be(:old_job_3) { create(:project_export_job, updated_at: 8.days.ago) }
+ let_it_be(:fresh_job_1) { create(:project_export_job, updated_at: 1.day.ago) }
+ let_it_be(:fresh_job_2) { create(:project_export_job, updated_at: 2.days.ago) }
+ let_it_be(:fresh_job_3) { create(:project_export_job, updated_at: 6.days.ago) }
+
+ let_it_be(:old_relation_export_1) { create(:project_relation_export, project_export_job_id: old_job_1.id) }
+ let_it_be(:old_relation_export_2) { create(:project_relation_export, project_export_job_id: old_job_2.id) }
+ let_it_be(:old_relation_export_3) { create(:project_relation_export, project_export_job_id: old_job_3.id) }
+ let_it_be(:fresh_relation_export_1) { create(:project_relation_export, project_export_job_id: fresh_job_1.id) }
+
+ let_it_be(:old_upload_1) { create(:relation_export_upload, project_relation_export_id: old_relation_export_1.id) }
+ let_it_be(:old_upload_2) { create(:relation_export_upload, project_relation_export_id: old_relation_export_2.id) }
+ let_it_be(:old_upload_3) { create(:relation_export_upload, project_relation_export_id: old_relation_export_3.id) }
+ let_it_be(:fresh_upload_1) do
+ create(
+ :relation_export_upload,
+ project_relation_export_id: fresh_relation_export_1.id
+ )
+ end
+
+ it 'prunes jobs and associations older than 7 days' do
+ expect { described_class.prune_expired_jobs }.to change { described_class.count }.by(-3)
+
+ expect(described_class.find_by(id: old_job_1.id)).to be_nil
+ expect(described_class.find_by(id: old_job_2.id)).to be_nil
+ expect(described_class.find_by(id: old_job_3.id)).to be_nil
+
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_1.id)).to be_nil
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_2.id)).to be_nil
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_3.id)).to be_nil
+
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_1.id)).to be_nil
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_2.id)).to be_nil
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_3.id)).to be_nil
+ end
+
+ it 'does not delete associated records for jobs younger than 7 days' do
+ described_class.prune_expired_jobs
+
+ expect(fresh_job_1.reload).to be_present
+ expect(fresh_job_2.reload).to be_present
+ expect(fresh_job_3.reload).to be_present
+ expect(fresh_relation_export_1.reload).to be_present
+ expect(fresh_upload_1.reload).to be_present
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index e800a468f8c..9a4179b9157 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -21,7 +21,6 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to belong_to(:creator).class_name('User') }
it { is_expected.to belong_to(:pool_repository) }
it { is_expected.to have_many(:users) }
- it { is_expected.to have_many(:integrations) }
it { is_expected.to have_many(:events) }
it { is_expected.to have_many(:merge_requests) }
it { is_expected.to have_many(:merge_request_metrics).class_name('MergeRequest::Metrics') }
@@ -150,6 +149,20 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout').with_foreign_key(:project_id) }
it { is_expected.to have_many(:pipeline_metadata).class_name('Ci::PipelineMetadata') }
it { is_expected.to have_many(:incident_management_timeline_event_tags).class_name('IncidentManagement::TimelineEventTag') }
+ it { is_expected.to have_many(:integrations) }
+ it { is_expected.to have_many(:push_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:tag_push_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:issue_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:confidential_issue_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:merge_request_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:note_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:confidential_note_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:job_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:archive_trace_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:pipeline_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:wiki_page_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:deployment_hooks_integrations).class_name('Integration') }
+ it { is_expected.to have_many(:alert_hooks_integrations).class_name('Integration') }
# GitLab Pages
it { is_expected.to have_many(:pages_domains) }
@@ -5758,6 +5771,32 @@ RSpec.describe Project, factory_default: :keep do
integration.project.execute_integrations(anything, :merge_request_hooks)
end
+
+ it 'does not trigger extra queries when called multiple times' do
+ integration.project.execute_integrations({}, :push_hooks)
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ integration.project.execute_integrations({}, :push_hooks)
+ end
+
+ expect(recorder.count).to be_zero
+ end
+
+ context 'with cache_project_integrations disabled' do
+ before do
+ stub_feature_flags(cache_project_integrations: false)
+ end
+
+ it 'triggers extra queries when called multiple times' do
+ integration.project.execute_integrations({}, :push_hooks)
+
+ recorder = ActiveRecord::QueryRecorder.new do
+ integration.project.execute_integrations({}, :push_hooks)
+ end
+
+ expect(recorder.count).not_to be_zero
+ end
+ end
end
describe '#has_active_hooks?' do
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
index 7e1af132b1d..741a0db3009 100644
--- a/spec/policies/merge_request_policy_spec.rb
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe MergeRequestPolicy do
let_it_be(:reporter) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:non_team_member) { create(:user) }
+ let_it_be(:bot) { create(:user, :project_bot) }
def permissions(user, merge_request)
described_class.new(user, merge_request)
@@ -72,6 +73,7 @@ RSpec.describe MergeRequestPolicy do
project.add_guest(guest)
project.add_guest(author)
project.add_developer(developer)
+ project.add_developer(bot)
end
context 'when merge request is public' do
@@ -95,6 +97,18 @@ RSpec.describe MergeRequestPolicy do
it do
is_expected.to be_allowed(:approve_merge_request)
end
+
+ it do
+ is_expected.to be_disallowed(:reset_merge_request_approvals)
+ end
+ end
+
+ context 'and the user is a bot' do
+ let(:user) { bot }
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
end
end
end
@@ -123,6 +137,14 @@ RSpec.describe MergeRequestPolicy do
it_behaves_like 'a denied user'
end
+
+ describe 'a bot' do
+ let(:subject) { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_disallowed(:reset_merge_request_approvals)
+ end
+ end
end
context 'when merge requests are private' do
@@ -144,6 +166,14 @@ RSpec.describe MergeRequestPolicy do
it_behaves_like 'a user with full access'
end
+
+ describe 'a bot' do
+ let(:subject) { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
context 'when merge request is unlocked' do
@@ -214,6 +244,7 @@ RSpec.describe MergeRequestPolicy do
group.add_guest(author)
group.add_reporter(reporter)
group.add_developer(developer)
+ group.add_developer(bot)
end
context 'when project is public' do
@@ -222,9 +253,25 @@ RSpec.describe MergeRequestPolicy do
describe 'the merge request author' do
subject { permissions(author, merge_request) }
- specify do
+ it do
is_expected.to be_allowed(:approve_merge_request)
end
+
+ it do
+ is_expected.to be_disallowed(:reset_merge_request_approvals)
+ end
+ end
+
+ describe 'a bot' do
+ subject { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_allowed(:approve_merge_request)
+ end
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
end
context 'and merge requests are private' do
@@ -250,6 +297,14 @@ RSpec.describe MergeRequestPolicy do
it_behaves_like 'a user with full access'
end
+
+ describe 'a bot' do
+ let(:subject) { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
end
@@ -273,6 +328,14 @@ RSpec.describe MergeRequestPolicy do
it_behaves_like 'a user with full access'
end
+
+ describe 'a bot' do
+ let(:subject) { permissions(bot, merge_request) }
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
end
@@ -297,11 +360,28 @@ RSpec.describe MergeRequestPolicy do
group_access: Gitlab::Access::DEVELOPER)
group.add_guest(non_team_member)
+ group.add_guest(bot)
end
- specify do
+ it do
is_expected.to be_allowed(:approve_merge_request)
end
+
+ it do
+ is_expected.to be_disallowed(:reset_merge_request_approvals)
+ end
+
+ context 'and the user is a bot' do
+ let(:user) { bot }
+
+ it do
+ is_expected.to be_allowed(:approve_merge_request)
+ end
+
+ it do
+ is_expected.to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
end
@@ -313,9 +393,25 @@ RSpec.describe MergeRequestPolicy do
subject { permissions(non_team_member, merge_request) }
- specify do
+ it do
is_expected.not_to be_allowed(:approve_merge_request)
end
+
+ it do
+ is_expected.not_to be_allowed(:reset_merge_request_approvals)
+ end
+
+ context 'and the user is a bot' do
+ subject { permissions(bot, merge_request) }
+
+ it do
+ is_expected.not_to be_allowed(:approve_merge_request)
+ end
+
+ it do
+ is_expected.not_to be_allowed(:reset_merge_request_approvals)
+ end
+ end
end
context 'when merge requests are disabled' do
diff --git a/spec/requests/api/merge_request_approvals_spec.rb b/spec/requests/api/merge_request_approvals_spec.rb
index b18f3017e03..ba0039f46f8 100644
--- a/spec/requests/api/merge_request_approvals_spec.rb
+++ b/spec/requests/api/merge_request_approvals_spec.rb
@@ -4,6 +4,8 @@ require 'spec_helper'
RSpec.describe API::MergeRequestApprovals do
let_it_be(:user) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) }
let_it_be(:approver) { create :user }
let_it_be(:group) { create :group }
@@ -87,4 +89,83 @@ RSpec.describe API::MergeRequestApprovals do
end
end
end
+
+ describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do
+ before do
+ merge_request.approvals.create!(user: user2)
+ create(:project_member, :maintainer, user: bot, source: project)
+ end
+
+ context 'for a bot user' do
+ it 'clears approvals of the merge_request' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:accepted)
+ expect(merge_request.approvals).to be_empty
+ end
+
+ context 'when bot user approved the merge request' do
+ before do
+ merge_request.approvals.create!(user: bot)
+ end
+
+ it 'clears approvals of the merge_request' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:accepted)
+ expect(merge_request.approvals).to be_empty
+ end
+ end
+ end
+
+ context 'for users with non-bot roles' do
+ let(:human_user) { create(:user) }
+
+ [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method|
+ it 'returns 401' do
+ project.send(role_method, human_user)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(merge_request.approvals.pluck(:user_id)).to contain_exactly(user2.id)
+ end
+ end
+ end
+
+ context 'for bot-users from external namespaces' do
+ let_it_be(:external_bot) { create(:user, :project_bot) }
+
+ context 'for external group bot-user' do
+ before do
+ create(:group_member, :maintainer, user: external_bot, source: create(:group))
+ end
+
+ it 'returns 401' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(merge_request.approvals.pluck(:user_id)).to contain_exactly(user2.id)
+ end
+ end
+
+ context 'for external project bot-user' do
+ before do
+ create(:project_member, :maintainer, user: external_bot, source: create(:project))
+ end
+
+ it 'returns 401' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
+
+ merge_request.reload
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(merge_request.approvals.pluck(:user_id)).to contain_exactly(user2.id)
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 0affc522d4e..aa0abe3fe64 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -9,7 +9,6 @@ RSpec.describe API::MergeRequests do
let_it_be(:user) { create(:user) }
let_it_be(:user2) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
- let_it_be(:bot) { create(:user, :project_bot) }
let_it_be(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone1) { create(:milestone, title: '0.9', project: project) }
@@ -3612,85 +3611,6 @@ RSpec.describe API::MergeRequests do
end
end
- describe 'PUT :id/merge_requests/:merge_request_iid/reset_approvals' do
- before do
- merge_request.approvals.create!(user: user2)
- create(:project_member, :maintainer, user: bot, source: project)
- end
-
- context 'when reset_approvals can be performed' do
- it 'clears approvals of the merge_request' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
-
- merge_request.reload
- expect(response).to have_gitlab_http_status(:accepted)
- expect(merge_request.approvals).to be_empty
- end
-
- context 'for users with non-bot roles' do
- let(:human_user) { create(:user) }
-
- [:add_owner, :add_maintainer, :add_developer, :add_guest].each do |role_method|
- it 'returns 401' do
- project.send(role_method, human_user)
-
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", human_user)
-
- merge_request.reload
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(merge_request.approvals.pluck(:user_id)).to eql([user2.id])
- end
- end
- end
-
- context 'for bot-users from external namespaces' do
- let_it_be(:external_bot) { create(:user, :project_bot) }
-
- context 'external group bot-user' do
- before do
- create(:group_member, :maintainer, user: external_bot, source: create(:group))
- end
-
- it 'returns 401' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
-
- merge_request.reload
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(merge_request.approvals.pluck(:user_id)).to eql([user2.id])
- end
- end
-
- context 'external project bot-user' do
- before do
- create(:project_member, :maintainer, user: external_bot, source: create(:project))
- end
-
- it 'returns 401' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", external_bot)
-
- merge_request.reload
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(merge_request.approvals.pluck(:user_id)).to eql([user2.id])
- end
- end
- end
-
- context 'for a bot user who approved the merge request' do
- before do
- merge_request.approvals.create!(user: bot)
- end
-
- it "returns 200" do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/reset_approvals", bot)
-
- merge_request.reload
- expect(response).to have_gitlab_http_status(:accepted)
- expect(merge_request.approvals).to be_empty
- end
- end
- end
- end
-
describe 'Time tracking' do
let!(:issuable) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, source_branch: 'markdown', title: "Test", created_at: base_time) }
diff --git a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
index cde906e64c2..2e528f7996c 100644
--- a/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/integrations/base_slack_notification_shared_examples.rb
@@ -34,7 +34,7 @@ RSpec.shared_examples Integrations::BaseSlackNotification do |factory:|
execute
end
- it_behaves_like 'Snowplow event tracking' do
+ it_behaves_like 'Snowplow event tracking with RedisHLL context' do
let(:feature_flag_name) { :route_hll_to_snowplow_phase2 }
let(:category) { described_class.to_s }
let(:action) { 'perform_integrations_action' }
diff --git a/spec/tooling/danger/stable_branch_spec.rb b/spec/tooling/danger/stable_branch_spec.rb
new file mode 100644
index 00000000000..08fd25b30e0
--- /dev/null
+++ b/spec/tooling/danger/stable_branch_spec.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+require 'gitlab-dangerfiles'
+require 'gitlab/dangerfiles/spec_helper'
+require 'rspec-parameterized'
+require 'httparty'
+
+require_relative '../../../tooling/danger/stable_branch'
+
+RSpec.describe Tooling::Danger::StableBranch, feature_category: :delivery do
+ using RSpec::Parameterized::TableSyntax
+
+ include_context 'with dangerfile'
+ let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) }
+
+ let(:stable_branch) { fake_danger.new(helper: fake_helper) }
+
+ describe '#check!' do
+ subject { stable_branch.check! }
+
+ shared_examples 'without a failure' do
+ it 'does not add a failure' do
+ expect(stable_branch).not_to receive(:fail)
+
+ subject
+ end
+ end
+
+ shared_examples 'with a failure' do |failure_message|
+ it 'fails' do
+ expect(stable_branch).to receive(:fail).with(failure_message)
+
+ subject
+ end
+ end
+
+ context 'when not applicable' do
+ where(:stable_branch?, :security_mr?) do
+ true | true
+ false | true
+ false | false
+ end
+
+ with_them do
+ before do
+ allow(fake_helper).to receive(:mr_target_branch).and_return(stable_branch? ? '15-1-stable-ee' : 'main')
+ allow(fake_helper).to receive(:security_mr?).and_return(security_mr?)
+ end
+
+ it_behaves_like "without a failure"
+ end
+ end
+
+ context 'when applicable' do
+ let(:target_branch) { '15-1-stable-ee' }
+ let(:feature_label_present) { false }
+ let(:bug_label_present) { true }
+ let(:response_success) { true }
+ let(:parsed_response) do
+ [
+ { 'version' => '15.1.1' },
+ { 'version' => '15.1.0' },
+ { 'version' => '15.0.2' },
+ { 'version' => '15.0.1' },
+ { 'version' => '15.0.0' },
+ { 'version' => '14.10.3' },
+ { 'version' => '14.10.2' },
+ { 'version' => '14.9.3' }
+ ]
+ end
+
+ let(:version_response) do
+ instance_double(
+ HTTParty::Response,
+ success?: response_success,
+ parsed_response: parsed_response
+ )
+ end
+
+ before do
+ allow(fake_helper).to receive(:mr_target_branch).and_return(target_branch)
+ allow(fake_helper).to receive(:security_mr?).and_return(false)
+ allow(fake_helper).to receive(:mr_has_labels?).with('type::feature').and_return(feature_label_present)
+ allow(fake_helper).to receive(:mr_has_labels?).with('type::bug').and_return(bug_label_present)
+ allow(HTTParty).to receive(:get).with(/page=1/).and_return(version_response)
+ end
+
+ # the stubbed behavior above is the success path
+ it_behaves_like "without a failure"
+
+ context 'with a feature label' do
+ let(:feature_label_present) { true }
+
+ it_behaves_like 'with a failure', described_class::FEATURE_ERROR_MESSAGE
+ end
+
+ context 'without a bug label' do
+ let(:bug_label_present) { false }
+
+ it_behaves_like 'with a failure', described_class::BUG_ERROR_MESSAGE
+ end
+
+ context 'when not an applicable version' do
+ let(:target_branch) { '14-9-stable-ee' }
+
+ it_behaves_like 'with a failure', described_class::VERSION_ERROR_MESSAGE
+ end
+
+ context 'when the version API request fails' do
+ let(:response_success) { false }
+
+ it 'adds a warning' do
+ expect(stable_branch).to receive(:warn).with(described_class::FAILED_VERSION_REQUEST_MESSAGE)
+
+ subject
+ end
+ end
+
+ context 'when more than one page of versions is needed' do
+ # we target a version we know will not be returned in the first request
+ let(:target_branch) { '14-10-stable-ee' }
+
+ let(:first_version_response) do
+ instance_double(
+ HTTParty::Response,
+ success?: response_success,
+ parsed_response: [
+ { 'version' => '15.1.1' },
+ { 'version' => '15.1.0' },
+ { 'version' => '15.0.2' },
+ { 'version' => '15.0.1' }
+ ]
+ )
+ end
+
+ let(:second_version_response) do
+ instance_double(
+ HTTParty::Response,
+ success?: response_success,
+ parsed_response: [
+ { 'version' => '15.0.0' },
+ { 'version' => '14.10.3' },
+ { 'version' => '14.10.2' },
+ { 'version' => '14.9.3' }
+ ]
+ )
+ end
+
+ before do
+ allow(HTTParty).to receive(:get).with(/page=1/).and_return(first_version_response)
+ allow(HTTParty).to receive(:get).with(/page=2/).and_return(second_version_response)
+ end
+
+ it_behaves_like "without a failure"
+ end
+
+ context 'when too many version API requests are made' do
+ let(:parsed_response) { [{ 'version' => '15.0.0' }] }
+
+ it 'adds a warning' do
+ expect(HTTParty).to receive(:get).and_return(version_response).at_least(10).times
+ expect(stable_branch).to receive(:warn).with(described_class::FAILED_VERSION_REQUEST_MESSAGE)
+
+ subject
+ end
+ end
+ end
+ end
+end
diff --git a/spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb b/spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb
new file mode 100644
index 00000000000..eded07c7a2f
--- /dev/null
+++ b/spec/workers/gitlab/export/prune_project_export_jobs_worker_spec.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Export::PruneProjectExportJobsWorker, feature_category: :importers do
+ let_it_be(:old_job_1) { create(:project_export_job, updated_at: 37.months.ago) }
+ let_it_be(:old_job_2) { create(:project_export_job, updated_at: 12.months.ago) }
+ let_it_be(:old_job_3) { create(:project_export_job, updated_at: 8.days.ago) }
+ let_it_be(:fresh_job_1) { create(:project_export_job, updated_at: 1.day.ago) }
+ let_it_be(:fresh_job_2) { create(:project_export_job, updated_at: 2.days.ago) }
+ let_it_be(:fresh_job_3) { create(:project_export_job, updated_at: 6.days.ago) }
+
+ let_it_be(:old_relation_export_1) { create(:project_relation_export, project_export_job_id: old_job_1.id) }
+ let_it_be(:old_relation_export_2) { create(:project_relation_export, project_export_job_id: old_job_2.id) }
+ let_it_be(:old_relation_export_3) { create(:project_relation_export, project_export_job_id: old_job_3.id) }
+ let_it_be(:fresh_relation_export_1) { create(:project_relation_export, project_export_job_id: fresh_job_1.id) }
+
+ let_it_be(:old_upload_1) { create(:relation_export_upload, project_relation_export_id: old_relation_export_1.id) }
+ let_it_be(:old_upload_2) { create(:relation_export_upload, project_relation_export_id: old_relation_export_2.id) }
+ let_it_be(:old_upload_3) { create(:relation_export_upload, project_relation_export_id: old_relation_export_3.id) }
+ let_it_be(:fresh_upload_1) { create(:relation_export_upload, project_relation_export_id: fresh_relation_export_1.id) }
+
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ include_examples 'an idempotent worker' do
+ it 'prunes jobs and associations older than 7 days' do
+ expect { perform_multiple }.to change { ProjectExportJob.count }.by(-3)
+ expect(ProjectExportJob.find_by(id: old_job_1.id)).to be_nil
+ expect(ProjectExportJob.find_by(id: old_job_2.id)).to be_nil
+ expect(ProjectExportJob.find_by(id: old_job_3.id)).to be_nil
+
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_1.id)).to be_nil
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_2.id)).to be_nil
+ expect(Projects::ImportExport::RelationExport.find_by(id: old_relation_export_3.id)).to be_nil
+
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_1.id)).to be_nil
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_2.id)).to be_nil
+ expect(Projects::ImportExport::RelationExportUpload.find_by(id: old_upload_3.id)).to be_nil
+ end
+
+ it 'leaves fresh jobs and associations' do
+ perform_multiple
+ expect(fresh_job_1.reload).to be_present
+ expect(fresh_job_2.reload).to be_present
+ expect(fresh_job_3.reload).to be_present
+ expect(fresh_relation_export_1.reload).to be_present
+ expect(fresh_upload_1.reload).to be_present
+ end
+ end
+ end
+end
diff --git a/tooling/danger/stable_branch.rb b/tooling/danger/stable_branch.rb
new file mode 100644
index 00000000000..6c0b94b4f06
--- /dev/null
+++ b/tooling/danger/stable_branch.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+module Tooling
+ module Danger
+ module StableBranch
+ VersionApiError = Class.new(StandardError)
+
+ STABLE_BRANCH_REGEX = %r{\A(?<version>\d+-\d+)-stable-ee\z}.freeze
+
+ # rubocop:disable Lint/MixedRegexpCaptureTypes
+ VERSION_REGEX = %r{
+ \A(?<major>\d+)
+ \.(?<minor>\d+)
+ (\.(?<patch>\d+))?
+ (-(?<rc>rc(?<rc_number>\d*)))?
+ (-\h+\.\h+)?
+ (-ee|\.ee\.\d+)?\z
+ }x.freeze
+ # rubocop:enable Lint/MixedRegexpCaptureTypes
+
+ MAINTENANCE_POLICY_URL = 'https://docs.gitlab.com/ee/policy/maintenance.html'
+
+ MAINTENANCE_POLICY_MESSAGE = <<~MSG
+ See the [release and maintenance policy](#{MAINTENANCE_POLICY_URL}) for more information.
+ MSG
+
+ FEATURE_ERROR_MESSAGE = <<~MSG
+ This MR includes the `type::feature` label. Features do not qualify for patch releases. #{MAINTENANCE_POLICY_MESSAGE}
+ MSG
+
+ BUG_ERROR_MESSAGE = <<~MSG
+ This branch is meant for backporting bug fixes. If this MR qualifies please add the `type::bug` label. #{MAINTENANCE_POLICY_MESSAGE}
+ MSG
+
+ VERSION_ERROR_MESSAGE = <<~MSG
+ Patches are only being accepted on the most recent 3 minor versions of GitLab. #{MAINTENANCE_POLICY_MESSAGE}
+ MSG
+
+ FAILED_VERSION_REQUEST_MESSAGE = <<~MSG
+ There was a problem checking if this is a qualified version for backporting. Re-running this job may fix the problem.
+ MSG
+
+ # rubocop:disable Style/SignalException
+ def check!
+ return unless stable_target_branch && !helper.security_mr?
+
+ fail FEATURE_ERROR_MESSAGE if has_feature_label?
+ fail BUG_ERROR_MESSAGE unless has_bug_label?
+ fail VERSION_ERROR_MESSAGE unless targeting_patchable_version?
+ end
+ # rubocop:enable Style/SignalException
+
+ private
+
+ def stable_target_branch
+ helper.mr_target_branch.match(STABLE_BRANCH_REGEX)
+ end
+
+ def has_feature_label?
+ helper.mr_has_labels?('type::feature')
+ end
+
+ def has_bug_label?
+ helper.mr_has_labels?('type::bug')
+ end
+
+ def targeting_patchable_version?
+ raise VersionApiError if last_three_minor_versions.empty?
+
+ last_three_minor_versions.include?(targeted_version)
+ rescue VersionApiError
+ # don't fail the job since we do not know the recent versions
+ warn FAILED_VERSION_REQUEST_MESSAGE
+ true
+ end
+
+ def last_three_minor_versions
+ return [] unless versions
+
+ current_version = versions.first.match(VERSION_REGEX)
+ version_1 = previous_minor_version(current_version)
+ version_2 = previous_minor_version(version_1)
+
+ [
+ version_to_minor_string(current_version),
+ version_to_minor_string(version_1),
+ version_to_minor_string(version_2)
+ ]
+ end
+
+ def targeted_version
+ stable_target_branch[1].tr('-', '.')
+ end
+
+ def versions(page = 1)
+ version_api_endpoint = "https://version.gitlab.com/api/v1/versions?per_page=50&page=#{page}"
+ response = HTTParty.get(version_api_endpoint) # rubocop:disable Gitlab/HTTParty
+
+ raise VersionApiError unless response.success?
+
+ version_list = response.parsed_response.map { |v| v['version'] } # rubocop:disable Rails/Pluck
+
+ version_list.sort_by { |v| Gem::Version.new(v) }.reverse
+ end
+
+ def previous_minor_version(version)
+ previous_minor = version[:minor].to_i - 1
+
+ return "#{version[:major]}.#{previous_minor}".match(VERSION_REGEX) if previous_minor >= 0
+
+ fetch_last_minor_version_for_major(version[:major].to_i - 1)
+ end
+
+ def fetch_last_minor_version_for_major(major)
+ page = 1
+ last_minor_version = nil
+
+ while last_minor_version.nil?
+ last_minor_version = versions(page).find do |version|
+ version.split('.').first.to_i == major
+ end
+
+ break if page > 10
+
+ page += 1
+ end
+
+ raise VersionApiError if last_minor_version.nil?
+
+ last_minor_version.match(VERSION_REGEX)
+ end
+
+ def version_to_minor_string(version)
+ "#{version[:major]}.#{version[:minor]}"
+ end
+ end
+ end
+end