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:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue37
-rw-r--r--app/assets/javascripts/issues_list/constants.js20
-rw-r--r--app/assets/javascripts/issues_list/index.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue110
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue58
-rw-r--r--app/graphql/queries/burndown_chart/burnup.query.graphql70
-rw-r--r--app/helpers/invite_members_helper.rb2
-rw-r--r--app/models/ci/build_trace_chunk.rb3
-rw-r--r--app/models/namespace.rb2
-rw-r--r--app/models/release.rb6
-rw-r--r--app/views/projects/_home_panel.html.haml79
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml4
-rw-r--r--changelogs/unreleased/21042-update-username-100.yml5
-rw-r--r--changelogs/unreleased/328442-convert-ci-build-trace-chunks-build-id-to-bigint.yml5
-rw-r--r--changelogs/unreleased/home-panel-tag-caching.yml5
-rw-r--r--changelogs/unreleased/ld-correct-copy-for-issue-hooks.yml6
-rw-r--r--config/feature_flags/development/cache_home_panel.yml8
-rw-r--r--config/feature_flags/development/validate_release_description_length.yml8
-rw-r--r--danger/changelog/Dangerfile2
-rw-r--r--db/migrate/20210427045604_initialize_conversion_of_ci_build_trace_chunks_to_bigint.rb16
-rw-r--r--db/post_migrate/20210427045711_backfill_ci_build_trace_chunks_for_bigint_conversion.rb26
-rw-r--r--db/schema_migrations/202104270456041
-rw-r--r--db/schema_migrations/202104270457111
-rw-r--r--db/structure.sql14
-rw-r--r--doc/administration/database_load_balancing.md2
-rw-r--r--doc/administration/gitaly/praefect.md2
-rw-r--r--doc/administration/nfs.md2
-rw-r--r--doc/administration/operations/rails_console.md2
-rw-r--r--doc/administration/packages/container_registry.md8
-rw-r--r--doc/administration/reference_architectures/25k_users.md2
-rw-r--r--doc/api/commits.md1
-rw-r--r--doc/api/deployments.md2
-rw-r--r--doc/api/discussions.md20
-rw-r--r--doc/api/epic_links.md2
-rw-r--r--doc/api/events.md2
-rw-r--r--doc/api/feature_flag_user_lists.md2
-rw-r--r--doc/api/feature_flags.md2
-rw-r--r--doc/api/geo_nodes.md3
-rw-r--r--doc/api/graphql/users_example.md2
-rw-r--r--doc/api/group_badges.md2
-rw-r--r--doc/api/groups.md2
-rw-r--r--doc/api/instance_clusters.md2
-rw-r--r--doc/api/issue_links.md8
-rw-r--r--doc/api/issues.md156
-rw-r--r--doc/api/jobs.md1
-rw-r--r--doc/api/keys.md6
-rw-r--r--doc/api/license.md2
-rw-r--r--doc/api/members.md2
-rw-r--r--doc/api/merge_request_approvals.md4
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/api/notification_settings.md2
-rw-r--r--doc/api/pipeline_schedules.md4
-rw-r--r--doc/api/pipelines.md4
-rw-r--r--doc/api/project_badges.md4
-rw-r--r--doc/api/project_import_export.md2
-rw-r--r--doc/api/project_repository_storage_moves.md5
-rw-r--r--doc/api/project_snippets.md2
-rw-r--r--doc/api/projects.md31
-rw-r--r--doc/api/releases/index.md24
-rw-r--r--doc/api/runners.md6
-rw-r--r--doc/api/services.md2
-rw-r--r--doc/api/users.md2
-rw-r--r--doc/ci/environments/deployment_safety.md2
-rw-r--r--doc/ci/troubleshooting.md2
-rw-r--r--doc/development/cascading_settings.md8
-rw-r--r--doc/development/gemfile.md57
-rw-r--r--doc/development/merge_request_performance_guidelines.md24
-rw-r--r--doc/development/testing_guide/best_practices.md2
-rw-r--r--doc/development/usage_ping/index.md16
-rw-r--r--doc/install/azure/index.md2
-rw-r--r--doc/integration/saml.md4
-rw-r--r--doc/ssh/README.md2
-rw-r--r--doc/user/clusters/agent/index.md2
-rw-r--r--doc/user/packages/npm_registry/index.md2
-rw-r--r--doc/user/project/import/svn.md4
-rw-r--r--lib/gitlab/database/background_migration/batch_optimizer.rb39
-rw-r--r--lib/gitlab/usage_data_counters/editor_unique_counter.rb1
-rw-r--r--locale/gitlab.pot31
-rw-r--r--package.json2
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb4
-rw-r--r--spec/frontend/issues_list/mock_data.js16
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js19
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js78
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js37
-rw-r--r--spec/helpers/invite_members_helper_spec.rb19
-rw-r--r--spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb53
-rw-r--r--spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb8
-rw-r--r--spec/models/namespace_spec.rb35
-rw-r--r--spec/models/release_spec.rb24
-rw-r--r--spec/services/alert_management/process_prometheus_alert_service_spec.rb236
-rw-r--r--spec/services/projects/alerting/notify_service_spec.rb258
-rw-r--r--spec/services/projects/prometheus/alerts/notify_service_spec.rb83
-rw-r--r--spec/support/shared_examples/alert_notification_service_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb161
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb177
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb52
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/services/alert_management_shared_examples.rb134
-rw-r--r--yarn.lock8
106 files changed, 1716 insertions, 887 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a1f074cd973..86d611f659a 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-f6f340eff91d01a1e36e8c9c368d93c9bff5e4f5
+22654ba48106412ca6680366afe6a47389458720
diff --git a/Gemfile b/Gemfile
index 1999b982c0b..a539a3c13d3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -482,7 +482,7 @@ gem 'gitaly', '~> 13.11.0.pre.rc1'
gem 'grpc', '~> 1.30.2'
-gem 'google-protobuf', '~> 3.14.0'
+gem 'google-protobuf', '~> 3.15.8'
gem 'toml-rb', '~> 1.0.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index be72969c089..369ae157e0e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -518,7 +518,7 @@ GEM
signet (~> 0.12)
google-cloud-env (1.4.0)
faraday (>= 0.17.3, < 2.0)
- google-protobuf (3.14.0)
+ google-protobuf (3.15.8)
googleapis-common-protos-types (1.0.5)
google-protobuf (~> 3.11)
googleauth (0.14.0)
@@ -1464,7 +1464,7 @@ DEPENDENCIES
gitlab_omniauth-ldap (~> 2.1.1)
gon (~> 6.4.0)
google-api-client (~> 0.33)
- google-protobuf (~> 3.14.0)
+ google-protobuf (~> 3.15.8)
gpgme (~> 2.0.19)
grape (~> 1.5.2)
grape-entity (~> 0.7.1)
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 3ccf982ef01..560f691eb4e 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -36,8 +36,10 @@ import { convertObjectPropsToCamelCase, getParameterByName } from '~/lib/utils/c
import { __ } from '~/locale';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
+import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
import eventHub from '../eventhub';
import IssueCardTimeInfo from './issue_card_time_info.vue';
@@ -88,6 +90,9 @@ export default {
hasIssues: {
default: false,
},
+ hasIssueWeightsFeature: {
+ default: false,
+ },
initialEmail: {
default: '',
},
@@ -103,6 +108,9 @@ export default {
newIssuePath: {
default: '',
},
+ projectIterationsPath: {
+ default: '',
+ },
projectLabelsPath: {
default: '',
},
@@ -155,7 +163,7 @@ export default {
return convertToSearchQuery(this.filterTokens) || undefined;
},
searchTokens() {
- return [
+ const tokens = [
{
type: 'author_username',
title: __('Author'),
@@ -216,6 +224,30 @@ export default {
],
},
];
+
+ if (this.projectIterationsPath) {
+ tokens.push({
+ type: 'iteration',
+ title: __('Iteration'),
+ icon: 'iteration',
+ token: IterationToken,
+ unique: true,
+ defaultIterations: [],
+ fetchIterations: this.fetchIterations,
+ });
+ }
+
+ if (this.hasIssueWeightsFeature) {
+ tokens.push({
+ type: 'weight',
+ title: __('Weight'),
+ icon: 'weight',
+ token: WeightToken,
+ unique: true,
+ });
+ }
+
+ return tokens;
},
showPaginationControls() {
return this.issues.length > 0;
@@ -273,6 +305,9 @@ export default {
fetchMilestones(search) {
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
},
+ fetchIterations(search) {
+ return axios.get(this.projectIterationsPath, { params: { search } });
+ },
fetchUsers(search) {
return axios.get(this.autocompleteUsersPath, { params: { search } });
},
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index 60a211ec8c0..38a0d953835 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -334,4 +334,24 @@ export const filters = {
[OPERATOR_IS]: 'confidential',
},
},
+ iteration: {
+ apiParam: {
+ [OPERATOR_IS]: 'iteration_title',
+ [OPERATOR_IS_NOT]: 'not[iteration_title]',
+ },
+ urlParam: {
+ [OPERATOR_IS]: 'iteration_title',
+ [OPERATOR_IS_NOT]: 'not[iteration_title]',
+ },
+ },
+ weight: {
+ apiParam: {
+ [OPERATOR_IS]: 'weight',
+ [OPERATOR_IS_NOT]: 'not[weight]',
+ },
+ urlParam: {
+ [OPERATOR_IS]: 'weight',
+ [OPERATOR_IS_NOT]: 'not[weight]',
+ },
+ },
};
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index 06c50a02ada..0318c2c2484 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -98,6 +98,7 @@ export function initIssuesListApp() {
maxAttachmentSize,
newIssuePath,
projectImportJiraPath,
+ projectIterationsPath,
projectLabelsPath,
projectMilestonesPath,
projectPath,
@@ -128,6 +129,7 @@ export function initIssuesListApp() {
issuesPath,
jiraIntegrationPath,
newIssuePath,
+ projectIterationsPath,
projectLabelsPath,
projectMilestonesPath,
projectPath,
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 3d8afd162cb..e2868879425 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
@@ -1,18 +1,16 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { __ } from '~/locale';
+export const DEBOUNCE_DELAY = 200;
+
const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') };
export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') };
export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') };
+export const DEFAULT_LABEL_CURRENT = { value: 'Current', text: __('Current') };
-export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL];
+export const DEFAULT_ITERATIONS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY, DEFAULT_LABEL_CURRENT];
-export const DEBOUNCE_DELAY = 200;
-
-export const SortDirection = {
- descending: 'descending',
- ascending: 'ascending',
-};
+export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL];
export const DEFAULT_MILESTONES = [
DEFAULT_LABEL_NONE,
@@ -21,4 +19,8 @@ export const DEFAULT_MILESTONES = [
{ value: 'Started', text: __('Started') },
];
+export const SortDirection = {
+ descending: 'descending',
+ ascending: 'ascending',
+};
/* eslint-enable @gitlab/require-i18n-strings */
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
new file mode 100644
index 00000000000..7b6a590279a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
@@ -0,0 +1,110 @@
+<script>
+import {
+ GlDropdownDivider,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchToken,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
+import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants';
+
+export default {
+ components: {
+ GlDropdownDivider,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchToken,
+ GlLoadingIcon,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ iterations: this.config.initialIterations || [],
+ defaultIterations: this.config.defaultIterations || DEFAULT_ITERATIONS,
+ loading: true,
+ };
+ },
+ computed: {
+ currentValue() {
+ return this.value.data;
+ },
+ activeIteration() {
+ return this.iterations.find((iteration) => iteration.title === this.currentValue);
+ },
+ },
+ watch: {
+ active: {
+ immediate: true,
+ handler(newValue) {
+ if (!newValue && !this.iterations.length) {
+ this.fetchIterationBySearchTerm(this.currentValue);
+ }
+ },
+ },
+ },
+ methods: {
+ fetchIterationBySearchTerm(searchTerm) {
+ const fetchPromise = this.config.fetchPath
+ ? this.config.fetchIterations(this.config.fetchPath, searchTerm)
+ : this.config.fetchIterations(searchTerm);
+
+ this.loading = true;
+
+ fetchPromise
+ .then((response) => {
+ this.iterations = Array.isArray(response) ? response : response.data;
+ })
+ .catch(() => createFlash({ message: __('There was a problem fetching iterations.') }))
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ searchIterations: debounce(function debouncedSearch({ data }) {
+ this.fetchIterationBySearchTerm(data);
+ }, DEBOUNCE_DELAY),
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="searchIterations"
+ >
+ <template #view="{ inputValue }">
+ {{ activeIteration ? activeIteration.title : inputValue }}
+ </template>
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="iteration in defaultIterations"
+ :key="iteration.value"
+ :value="iteration.value"
+ >
+ {{ iteration.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultIterations.length" />
+ <gl-loading-icon v-if="loading" />
+ <template v-else>
+ <gl-filtered-search-suggestion
+ v-for="iteration in iterations"
+ :key="iteration.title"
+ :value="iteration.title"
+ >
+ {{ iteration.title }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
new file mode 100644
index 00000000000..cfad79b9afa
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/weight_token.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDropdownDivider, GlFilteredSearchSuggestion, GlFilteredSearchToken } from '@gitlab/ui';
+import { DEFAULT_LABEL_ANY, DEFAULT_LABEL_NONE } from '../constants';
+
+export default {
+ baseWeights: ['0', '1', '2', '3', '4', '5'],
+ components: {
+ GlDropdownDivider,
+ GlFilteredSearchSuggestion,
+ GlFilteredSearchToken,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ weights: this.$options.baseWeights,
+ defaultWeights: this.config.defaultWeights || [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY],
+ };
+ },
+ methods: {
+ updateWeights({ data }) {
+ const weight = parseInt(data, 10);
+ this.weights = Number.isNaN(weight) ? this.$options.baseWeights : [String(weight)];
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :config="config"
+ v-bind="{ ...$props, ...$attrs }"
+ v-on="$listeners"
+ @input="updateWeights"
+ >
+ <template #suggestions>
+ <gl-filtered-search-suggestion
+ v-for="weight in defaultWeights"
+ :key="weight.value"
+ :value="weight.value"
+ >
+ {{ weight.text }}
+ </gl-filtered-search-suggestion>
+ <gl-dropdown-divider v-if="defaultWeights.length" />
+ <gl-filtered-search-suggestion v-for="weight of weights" :key="weight" :value="weight">
+ {{ weight }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/graphql/queries/burndown_chart/burnup.query.graphql b/app/graphql/queries/burndown_chart/burnup.query.graphql
new file mode 100644
index 00000000000..7a389a6def5
--- /dev/null
+++ b/app/graphql/queries/burndown_chart/burnup.query.graphql
@@ -0,0 +1,70 @@
+query BurnupTimesSeriesData($id: ID!, $isIteration: Boolean = false, $weight: Boolean = false) {
+ milestone(id: $id) @skip(if: $isIteration) {
+ __typename
+ id
+ title
+ report {
+ __typename
+ burnupTimeSeries {
+ __typename
+ date
+ completedCount @skip(if: $weight)
+ scopeCount @skip(if: $weight)
+ completedWeight @include(if: $weight)
+ scopeWeight @include(if: $weight)
+ }
+ stats {
+ __typename
+ total {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ complete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ incomplete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ }
+ }
+ }
+ iteration(id: $id) @include(if: $isIteration) {
+ __typename
+ id
+ title
+ report {
+ __typename
+ burnupTimeSeries {
+ __typename
+ date
+ completedCount @skip(if: $weight)
+ scopeCount @skip(if: $weight)
+ completedWeight @include(if: $weight)
+ scopeWeight @include(if: $weight)
+ }
+ stats {
+ __typename
+ total {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ complete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ incomplete {
+ __typename
+ count @skip(if: $weight)
+ weight @include(if: $weight)
+ }
+ }
+ }
+ }
+}
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index 2cad6f4745c..6e4de4e90b0 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -8,7 +8,7 @@ module InviteMembersHelper
end
def can_invite_members_for_project?(project)
- Feature.enabled?(:invite_members_group_modal, project.group) && can_import_members?
+ Feature.enabled?(:invite_members_group_modal, project.group) && can_manage_project_members?(project)
end
def directly_invite_members?
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 7e03d709f24..719511bbb8a 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -8,6 +8,9 @@ module Ci
include ::Checksummable
include ::Gitlab::ExclusiveLeaseHelpers
include ::Gitlab::OptimisticLocking
+ include IgnorableColumns
+
+ ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 16e65aa0fe6..8777d0081cc 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -198,7 +198,7 @@ class Namespace < ApplicationRecord
end
def any_project_has_container_registry_tags?
- all_projects.any?(&:has_container_registry_tags?)
+ all_projects.includes(:container_repositories).any?(&:has_container_registry_tags?)
end
def first_project_with_container_registry_tags
diff --git a/app/models/release.rb b/app/models/release.rb
index 5037a1558ca..95de30523e1 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -24,6 +24,7 @@ class Release < ApplicationRecord
before_create :set_released_at
validates :project, :tag, presence: true
+ validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, if: :should_validate_description_length?
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
validates :links, nested_attributes_duplicates: { scope: :release, child_attributes: %i[name url filepath] }
@@ -101,6 +102,11 @@ class Release < ApplicationRecord
private
+ def should_validate_description_length?
+ description_changed? &&
+ ::Feature.enabled?(:validate_release_description_length, project, default_enabled: :yaml)
+ end
+
def actual_sha
sha || actual_tag&.dereferenced_target
end
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index b2380a3ba57..61cd8189551 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -2,6 +2,7 @@
- show_auto_devops_callout = show_auto_devops_callout?(@project)
- max_project_topic_length = 15
- emails_disabled = @project.emails_disabled?
+- cache_enabled = Feature.enabled?(:cache_home_panel, type: :development, default_enabled: :yaml)
.project-home-panel.js-show-on-project-root.gl-my-5{ class: [("empty-project" if empty_repo)] }
.gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-mb-3
@@ -23,42 +24,45 @@
- if current_user
%span.access-request-links.gl-ml-3
= render 'shared/members/access_request_links', source: @project
- - if @project.tag_list.present?
- %span.home-panel-topic-list.mt-2.w-100.d-inline-flex.gl-font-base.gl-font-weight-normal.gl-align-items-center
- = sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2')
- - @project.topics_to_show.each do |topic|
- - project_topics_classes = "badge badge-pill badge-secondary gl-mr-2"
- - explore_project_topic_path = explore_projects_path(tag: topic)
- - if topic.length > max_project_topic_length
- %a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
- = topic.titleize
- - else
- %a{ class: project_topics_classes, href: explore_project_topic_path, itemprop: 'keywords' }
- = topic.titleize
+ - if @project.tag_list.present?
+ = cache_if(cache_enabled, [@project, :tag_list], expires_in: 1.day) do
+ %span.home-panel-topic-list.mt-2.w-100.d-inline-flex.gl-font-base.gl-font-weight-normal.gl-align-items-center
+ = sprite_icon('tag', css_class: 'icon gl-relative gl-mr-2')
- - if @project.has_extra_topics?
- .text-nowrap.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.topics_not_shown.join(', ') : nil }
- = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown }
+ - @project.topics_to_show.each do |topic|
+ - project_topics_classes = "badge badge-pill badge-secondary gl-mr-2"
+ - explore_project_topic_path = explore_projects_path(tag: topic)
+ - if topic.length > max_project_topic_length
+ %a{ class: "#{ project_topics_classes } str-truncated-30 has-tooltip", data: { container: "body" }, title: topic, href: explore_project_topic_path, itemprop: 'keywords' }
+ = topic.titleize
+ - else
+ %a{ class: project_topics_classes, href: explore_project_topic_path, itemprop: 'keywords' }
+ = topic.titleize
+ - if @project.has_extra_topics?
+ .text-nowrap.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_topics? ? @project.topics_not_shown.join(', ') : nil }
+ = _("+ %{count} more") % { count: @project.count_of_extra_topics_not_shown }
- .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5
- - if current_user
- .gl-display-flex.gl-align-items-start.gl-mr-3
- - if @notification_setting
- .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } }
+ = cache_if(cache_enabled, [@project, :buttons, current_user, @notification_setting], expires_in: 1.day) do
+ .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5
+ - if current_user
+ .gl-display-flex.gl-align-items-start.gl-mr-3
+ - if @notification_setting
+ .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } }
- .count-buttons.gl-display-flex.gl-align-items-flex-start
- = render 'projects/buttons/star'
- = render 'projects/buttons/fork'
+ .count-buttons.gl-display-flex.gl-align-items-flex-start
+ = render 'projects/buttons/star'
+ = render 'projects/buttons/fork'
- if can?(current_user, :download_code, @project)
- %nav.project-stats
- .nav-links.quick-links
- - if @project.empty_repo?
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
- - else
- = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
+ = cache_if(cache_enabled, [@project, :download_code], expires_in: 1.minute) do
+ %nav.project-stats
+ .nav-links.quick-links
+ - if @project.empty_repo?
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
+ - else
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
.home-panel-home-desc.mt-1
- if @project.description.present?
@@ -80,11 +84,12 @@
= render_if_exists "projects/home_mirror"
- if @project.badges.present?
- .project-badges.mb-2
- - @project.badges.each do |badge|
- %a.gl-mr-3{ href: badge.rendered_link_url(@project),
- target: '_blank',
- rel: 'noopener noreferrer' }>
- %img.project-badge{ src: badge.rendered_image_url(@project),
- 'aria-hidden': true,
- alt: 'Project badge' }>
+ = cache_if(cache_enabled, [@project, :badges], expires_in: 1.day) do
+ .project-badges.mb-2
+ - @project.badges.each do |badge|
+ %a.gl-mr-3{ href: badge.rendered_link_url(@project),
+ target: '_blank',
+ rel: 'noopener noreferrer' }>
+ %img.project-badge{ src: badge.rendered_image_url(@project),
+ 'aria-hidden': true,
+ alt: 'Project badge' }>
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 2185df3a994..5a6c2c5faaf 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -4,6 +4,8 @@
- page_description @milestone.description_html
- add_page_specific_style 'page_bundles/milestone'
+- add_page_startup_api_call milestone_tab_path(@milestone, 'issues', show_project_name: false)
+
= render 'shared/milestones/header', milestone: @milestone
= render 'shared/milestones/description', milestone: @milestone
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index ad84ce1d343..18912bf149f 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -43,13 +43,13 @@
= form.label :issues_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Issues events')
%p.text-muted.gl-ml-1
- = s_('Webhooks|URL is triggered when an issue is created, updated, or merged')
+ = s_('Webhooks|URL is triggered when an issue is created, updated, closed, or reopened')
%li
= form.check_box :confidential_issues_events, class: 'form-check-input'
= form.label :confidential_issues_events, class: 'list-label form-check-label gl-ml-1' do
%strong= s_('Webhooks|Confidential issues events')
%p.text-muted.gl-ml-1
- = s_('Webhooks|URL is triggered when a confidential issue is created, updated, or merged')
+ = s_('Webhooks|URL is triggered when a confidential issue is created, updated, closed, or reopened')
- if @group
= render_if_exists 'groups/hooks/member_events', form: form
= render_if_exists 'groups/hooks/subgroup_events', form: form
diff --git a/changelogs/unreleased/21042-update-username-100.yml b/changelogs/unreleased/21042-update-username-100.yml
new file mode 100644
index 00000000000..691c53eceb6
--- /dev/null
+++ b/changelogs/unreleased/21042-update-username-100.yml
@@ -0,0 +1,5 @@
+---
+title: Fix N+1 queries in namespace#any_project_has_container_registry_tags?
+merge_request: 59916
+author:
+type: performance
diff --git a/changelogs/unreleased/328442-convert-ci-build-trace-chunks-build-id-to-bigint.yml b/changelogs/unreleased/328442-convert-ci-build-trace-chunks-build-id-to-bigint.yml
new file mode 100644
index 00000000000..8ee3745d56b
--- /dev/null
+++ b/changelogs/unreleased/328442-convert-ci-build-trace-chunks-build-id-to-bigint.yml
@@ -0,0 +1,5 @@
+---
+title: Initialize conversion of ci_build_trace_chunks.build_id to bigint
+merge_request: 60346
+author:
+type: other
diff --git a/changelogs/unreleased/home-panel-tag-caching.yml b/changelogs/unreleased/home-panel-tag-caching.yml
new file mode 100644
index 00000000000..29b756add9d
--- /dev/null
+++ b/changelogs/unreleased/home-panel-tag-caching.yml
@@ -0,0 +1,5 @@
+---
+title: Cache project tag list
+merge_request: 57031
+author:
+type: performance
diff --git a/changelogs/unreleased/ld-correct-copy-for-issue-hooks.yml b/changelogs/unreleased/ld-correct-copy-for-issue-hooks.yml
new file mode 100644
index 00000000000..0622d642aab
--- /dev/null
+++ b/changelogs/unreleased/ld-correct-copy-for-issue-hooks.yml
@@ -0,0 +1,6 @@
+---
+title: Fix copy on webhook admin pages for "Issues events" and "Confidential issues
+ events"
+merge_request: 60453
+author:
+type: changed
diff --git a/config/feature_flags/development/cache_home_panel.yml b/config/feature_flags/development/cache_home_panel.yml
new file mode 100644
index 00000000000..63798cd31d0
--- /dev/null
+++ b/config/feature_flags/development/cache_home_panel.yml
@@ -0,0 +1,8 @@
+---
+name: cache_home_panel
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57031
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328421
+milestone: '13.12'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/feature_flags/development/validate_release_description_length.yml b/config/feature_flags/development/validate_release_description_length.yml
new file mode 100644
index 00000000000..6a7b3a937ca
--- /dev/null
+++ b/config/feature_flags/development/validate_release_description_length.yml
@@ -0,0 +1,8 @@
+---
+name: validate_release_description_length
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60380
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329192
+milestone: '13.12'
+type: development
+group: group::release
+default_enabled: false
diff --git a/danger/changelog/Dangerfile b/danger/changelog/Dangerfile
index 1dd58abd9f0..4c0ce5c44b5 100644
--- a/danger/changelog/Dangerfile
+++ b/danger/changelog/Dangerfile
@@ -97,7 +97,7 @@ elsif changelog.optional?
message changelog.optional_text
end
-if changelog.required? || changelog.optional?
+if helper.ci? && (changelog.required? || changelog.optional?)
checked = 0
git.commits.each do |commit|
diff --git a/db/migrate/20210427045604_initialize_conversion_of_ci_build_trace_chunks_to_bigint.rb b/db/migrate/20210427045604_initialize_conversion_of_ci_build_trace_chunks_to_bigint.rb
new file mode 100644
index 00000000000..ec3bb0b7e45
--- /dev/null
+++ b/db/migrate/20210427045604_initialize_conversion_of_ci_build_trace_chunks_to_bigint.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class InitializeConversionOfCiBuildTraceChunksToBigint < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ TABLE = :ci_build_trace_chunks
+ COLUMNS = %i(build_id)
+
+ def up
+ initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+
+ def down
+ revert_initialize_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+end
diff --git a/db/post_migrate/20210427045711_backfill_ci_build_trace_chunks_for_bigint_conversion.rb b/db/post_migrate/20210427045711_backfill_ci_build_trace_chunks_for_bigint_conversion.rb
new file mode 100644
index 00000000000..4c656f56a32
--- /dev/null
+++ b/db/post_migrate/20210427045711_backfill_ci_build_trace_chunks_for_bigint_conversion.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class BackfillCiBuildTraceChunksForBigintConversion < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ TABLE = :ci_build_trace_chunks
+ COLUMNS = %i(build_id)
+
+ def up
+ return unless should_run?
+
+ backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+
+ def down
+ return unless should_run?
+
+ revert_backfill_conversion_of_integer_to_bigint(TABLE, COLUMNS)
+ end
+
+ private
+
+ def should_run?
+ Gitlab.dev_or_test_env? || Gitlab.com?
+ end
+end
diff --git a/db/schema_migrations/20210427045604 b/db/schema_migrations/20210427045604
new file mode 100644
index 00000000000..6cb29994a87
--- /dev/null
+++ b/db/schema_migrations/20210427045604
@@ -0,0 +1 @@
+bdeb78403607d45d5eb779623d0e2aa1acf026f6aced6f1134824a35dfec7e74 \ No newline at end of file
diff --git a/db/schema_migrations/20210427045711 b/db/schema_migrations/20210427045711
new file mode 100644
index 00000000000..bb713fc08d0
--- /dev/null
+++ b/db/schema_migrations/20210427045711
@@ -0,0 +1 @@
+3cd56794ac903d9598863215a34eda62c3dc96bed78bed5b8a99fc522e319b35 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index a78b9656f28..f52f5457dd2 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -162,6 +162,15 @@ BEGIN
END;
$$;
+CREATE FUNCTION trigger_cf2f9e35f002() RETURNS trigger
+ LANGUAGE plpgsql
+ AS $$
+BEGIN
+ NEW."build_id_convert_to_bigint" := NEW."build_id";
+ RETURN NEW;
+END;
+$$;
+
CREATE TABLE audit_events (
id bigint NOT NULL,
author_id integer NOT NULL,
@@ -10370,7 +10379,8 @@ CREATE TABLE ci_build_trace_chunks (
data_store integer NOT NULL,
raw_data bytea,
checksum bytea,
- lock_version integer DEFAULT 0 NOT NULL
+ lock_version integer DEFAULT 0 NOT NULL,
+ build_id_convert_to_bigint bigint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE ci_build_trace_chunks_id_seq
@@ -24819,6 +24829,8 @@ CREATE TRIGGER trigger_8485e97c00e3 BEFORE INSERT OR UPDATE ON ci_sources_pipeli
CREATE TRIGGER trigger_be1804f21693 BEFORE INSERT OR UPDATE ON ci_job_artifacts FOR EACH ROW EXECUTE PROCEDURE trigger_be1804f21693();
+CREATE TRIGGER trigger_cf2f9e35f002 BEFORE INSERT OR UPDATE ON ci_build_trace_chunks FOR EACH ROW EXECUTE PROCEDURE trigger_cf2f9e35f002();
+
CREATE TRIGGER trigger_has_external_issue_tracker_on_delete AFTER DELETE ON services FOR EACH ROW WHEN ((((old.category)::text = 'issue_tracker'::text) AND (old.active = true) AND (old.project_id IS NOT NULL))) EXECUTE PROCEDURE set_has_external_issue_tracker();
CREATE TRIGGER trigger_has_external_issue_tracker_on_insert AFTER INSERT ON services FOR EACH ROW WHEN ((((new.category)::text = 'issue_tracker'::text) AND (new.active = true) AND (new.project_id IS NOT NULL))) EXECUTE PROCEDURE set_has_external_issue_tracker();
diff --git a/doc/administration/database_load_balancing.md b/doc/administration/database_load_balancing.md
index 56079fba54b..bd34a82f688 100644
--- a/doc/administration/database_load_balancing.md
+++ b/doc/administration/database_load_balancing.md
@@ -208,7 +208,7 @@ response timings.
## Primary sticking
After a write has been performed, GitLab sticks to using the primary for a
-certain period of time, scoped to the user that performed the write. GitLab
+certain period of time, scoped to the user that performed the write. GitLab
reverts back to using secondaries when they have either caught up, or after 30
seconds.
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index e15be127211..14e5710e1ee 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -1453,7 +1453,7 @@ To determine the current primary Gitaly node for a specific Praefect node:
- Use the `Shard Primary Election` [Grafana chart](#grafana) on the [`Gitlab Omnibus - Praefect` dashboard](https://gitlab.com/gitlab-org/grafana-dashboards/-/blob/master/omnibus/praefect.json).
This is recommended.
- If you do not have Grafana set up, use the following command on each host of each
- Praefect node:
+ Praefect node:
```shell
curl localhost:9652/metrics | grep gitaly_praefect_primaries`
diff --git a/doc/administration/nfs.md b/doc/administration/nfs.md
index 602b7ec220e..c49a2c20ed2 100644
--- a/doc/administration/nfs.md
+++ b/doc/administration/nfs.md
@@ -22,7 +22,7 @@ file system performance, see
## Gitaly and NFS deprecation
WARNING:
-From GitLab 14.0, enhancements and bug fixes for NFS for Git repositories are no longer
+From GitLab 14.0, enhancements and bug fixes for NFS for Git repositories are no longer
considered and customer technical support is considered out of scope.
[Read more about Gitaly and NFS](gitaly/index.md#nfs-deprecation-notice) and
[the correct mount options to use](#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
diff --git a/doc/administration/operations/rails_console.md b/doc/administration/operations/rails_console.md
index c9e5253fbd1..8c366e311b8 100644
--- a/doc/administration/operations/rails_console.md
+++ b/doc/administration/operations/rails_console.md
@@ -152,7 +152,7 @@ Traceback (most recent call last):
In case you encounter a similar error to this:
```plaintext
-[root ~]# sudo gitlab-rails runner helloworld.rb
+[root ~]# sudo gitlab-rails runner helloworld.rb
Please specify a valid ruby command or the path of a script to run.
Run 'rails runner -h' for help.
diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md
index e84811db30f..d8c018c83f8 100644
--- a/doc/administration/packages/container_registry.md
+++ b/doc/administration/packages/container_registry.md
@@ -310,7 +310,7 @@ and GCS, this transfer is achieved with a copy followed by a delete. With object
these deleted temporary upload artifacts are kept as non-current versions, therefore increasing the
storage bucket size. To ensure that non-current versions are deleted after a given amount of time,
you should configure an object lifecycle policy with your storage provider.
-
+
You can configure the Container Registry to use various storage backends by
configuring a storage driver. By default the GitLab Container Registry
is configured to use the [file system driver](#use-file-system)
@@ -1075,15 +1075,15 @@ If the registry fails to authenticate valid login attempts, you get the followin
```shell
# docker login gitlab.company.com:4567
Username: user
-Password:
+Password:
Error response from daemon: login attempt to https://gitlab.company.com:4567/v2/ failed with status: 401 Unauthorized
```
And more specifically, this appears in the `/var/log/gitlab/registry/current` log file:
```plaintext
-level=info msg="token signed by untrusted key with ID: "TOKE:NL6Q:7PW6:EXAM:PLET:OKEN:BG27:RCIB:D2S3:EXAM:PLET:OKEN""
-level=warning msg="error authorizing context: invalid token" go.version=go1.12.7 http.request.host="gitlab.company.com:4567" http.request.id=74613829-2655-4f96-8991-1c9fe33869b8 http.request.method=GET http.request.remoteaddr=10.72.11.20 http.request.uri="/v2/" http.request.useragent="docker/19.03.2 go/go1.12.8 git-commit/6a30dfc kernel/3.10.0-693.2.2.el7.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/19.03.2 \(linux\))"
+level=info msg="token signed by untrusted key with ID: "TOKE:NL6Q:7PW6:EXAM:PLET:OKEN:BG27:RCIB:D2S3:EXAM:PLET:OKEN""
+level=warning msg="error authorizing context: invalid token" go.version=go1.12.7 http.request.host="gitlab.company.com:4567" http.request.id=74613829-2655-4f96-8991-1c9fe33869b8 http.request.method=GET http.request.remoteaddr=10.72.11.20 http.request.uri="/v2/" http.request.useragent="docker/19.03.2 go/go1.12.8 git-commit/6a30dfc kernel/3.10.0-693.2.2.el7.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/19.03.2 \(linux\))"
```
GitLab uses the contents of the certificate key pair's two sides to encrypt the authentication token
diff --git a/doc/administration/reference_architectures/25k_users.md b/doc/administration/reference_architectures/25k_users.md
index fdc23d3e9b3..129386d6ce5 100644
--- a/doc/administration/reference_architectures/25k_users.md
+++ b/doc/administration/reference_architectures/25k_users.md
@@ -34,7 +34,7 @@ full list of reference architectures, see
| Monitoring node | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
| Object storage | n/a | n/a | n/a | n/a | n/a |
| NFS server | 1 | 4 vCPU, 3.6 GB memory | `n1-highcpu-4` | `c5.xlarge` | `F4s v2` |
-
+
NOTE:
Components marked with * can be optionally run on reputable
third party external PaaS PostgreSQL solutions. Google Cloud SQL and AWS RDS are known to work.
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 117f949aba0..22d98b2b0a6 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -316,7 +316,6 @@ Example response:
{
"id": "8b090c1b79a14f2bd9e8a738f717824ff53aebad",
"short_id": "8b090c1b",
- "title": "Feature added",
"author_name": "Example User",
"author_email": "user@example.com",
"authored_date": "2016-12-12T20:10:39.000+01:00",
diff --git a/doc/api/deployments.md b/doc/api/deployments.md
index 32d3ab55f9f..cf224ad60ab 100644
--- a/doc/api/deployments.md
+++ b/doc/api/deployments.md
@@ -266,7 +266,7 @@ Example of response
"status": "success",
"updated_at": "2016-08-11T07:43:52.143Z",
"web_url": "http://gitlab.dev/root/project/pipelines/5"
- }
+ },
"runner": null
}
}
diff --git a/doc/api/discussions.md b/doc/api/discussions.md
index 828370c3386..ef3c7e82b19 100644
--- a/doc/api/discussions.md
+++ b/doc/api/discussions.md
@@ -270,7 +270,7 @@ GET /projects/:id/snippets/:snippet_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Snippet",
- "noteable_id": null
+ "noteable_iid": null
},
{
"id": 1129,
@@ -290,7 +290,7 @@ GET /projects/:id/snippets/:snippet_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Snippet",
- "noteable_id": null,
+ "noteable_iid": null,
"resolvable": false
}
]
@@ -317,7 +317,7 @@ GET /projects/:id/snippets/:snippet_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Snippet",
- "noteable_id": null,
+ "noteable_iid": null,
"resolvable": false
}
]
@@ -476,7 +476,7 @@ GET /groups/:id/epics/:epic_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Epic",
- "noteable_id": null,
+ "noteable_iid": null,
"resolvable": false
},
{
@@ -497,7 +497,7 @@ GET /groups/:id/epics/:epic_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Epic",
- "noteable_id": null,
+ "noteable_iid": null,
"resolvable": false
}
]
@@ -524,7 +524,7 @@ GET /groups/:id/epics/:epic_id/discussions
"system": false,
"noteable_id": 3,
"noteable_type": "Epic",
- "noteable_id": null,
+ "noteable_iid": null,
"resolvable": false
}
]
@@ -757,7 +757,7 @@ Diff comments also contain position:
"notes": [
{
"id": 1128,
- "type": DiffNote,
+ "type": "DiffNote",
"body": "diff comment",
"attachment": null,
"author": {
@@ -787,12 +787,12 @@ Diff comments also contain position:
"line_range": {
"start": {
"line_code": "588440f66559714280628a4f9799f0c4eb880a4a_10_10",
- "type": "new",
+ "type": "new"
},
"end": {
"line_code": "588440f66559714280628a4f9799f0c4eb880a4a_11_11",
"type": "old"
- },
+ }
}
},
"resolved": false,
@@ -1089,7 +1089,7 @@ Diff comments contain also position:
"notes": [
{
"id": 1128,
- "type": DiffNote,
+ "type": "DiffNote",
"body": "diff comment",
"attachment": null,
"author": {
diff --git a/doc/api/epic_links.md b/doc/api/epic_links.md
index 228f68531fd..8198130c61e 100644
--- a/doc/api/epic_links.md
+++ b/doc/api/epic_links.md
@@ -97,7 +97,7 @@ Example response:
"id": 6,
"iid": 38,
"group_id": 1,
- "parent_id": 5
+ "parent_id": 5,
"title": "Accusamus iste et ullam ratione voluptatem omnis debitis dolor est.",
"description": "Molestias dolorem eos vitae expedita impedit necessitatibus quo voluptatum.",
"author": {
diff --git a/doc/api/events.md b/doc/api/events.md
index befc1164432..5664d97d7db 100644
--- a/doc/api/events.md
+++ b/doc/api/events.md
@@ -301,7 +301,7 @@ Example response:
```json
[
{
- "id": 8
+ "id": 8,
"title":null,
"project_id":1,
"action_name":"opened",
diff --git a/doc/api/feature_flag_user_lists.md b/doc/api/feature_flag_user_lists.md
index e0a5fe99663..af8b3fcc71e 100644
--- a/doc/api/feature_flag_user_lists.md
+++ b/doc/api/feature_flag_user_lists.md
@@ -126,7 +126,7 @@ Example response:
"iid": 1,
"project_id": 1,
"created_at": "2020-02-04T08:13:10.507Z",
- "updated_at": "2020-02-04T08:13:10.507Z",
+ "updated_at": "2020-02-04T08:13:10.507Z"
}
```
diff --git a/doc/api/feature_flags.md b/doc/api/feature_flags.md
index fa5481b12a7..50cb9b1141e 100644
--- a/doc/api/feature_flags.md
+++ b/doc/api/feature_flags.md
@@ -70,7 +70,7 @@ Example response:
"version": "new_version_flag",
"created_at":"2019-11-04T08:13:10.507Z",
"updated_at":"2019-11-04T08:13:10.507Z",
- "scopes":[]
+ "scopes":[],
"strategies": [
{
"id": 2,
diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md
index 38f11d8dfe2..7926aeeeed7 100644
--- a/doc/api/geo_nodes.md
+++ b/doc/api/geo_nodes.md
@@ -362,9 +362,6 @@ Example response:
"wikis_checksum_mismatch_count": 1,
"repositories_retrying_verification_count": 1,
"wikis_retrying_verification_count": 3,
- "repositories_checked_count": 7,
- "repositories_checked_failed_count": 2,
- "repositories_checked_in_percentage": "17.07%",
"last_event_id": 23,
"last_event_timestamp": 1509681166,
"cursor_last_event_id": null,
diff --git a/doc/api/graphql/users_example.md b/doc/api/graphql/users_example.md
index 78703c3ce89..1222cd8ee8e 100644
--- a/doc/api/graphql/users_example.md
+++ b/doc/api/graphql/users_example.md
@@ -68,7 +68,7 @@ explorer. GraphiQL explorer is available for:
}
}
}
- }
+ }
```
1. Open the [GraphiQL explorer tool](https://gitlab.com/-/graphql-explorer).
diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md
index 881054ef44b..848d5735096 100644
--- a/doc/api/group_badges.md
+++ b/doc/api/group_badges.md
@@ -192,6 +192,6 @@ Example response:
"link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
"image_url": "https://shields.io/my/badge",
"rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
- "rendered_image_url": "https://shields.io/my/badge",
+ "rendered_image_url": "https://shields.io/my/badge"
}
```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 728d5149e73..b1e1e2bb6c5 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -101,7 +101,7 @@ GET /groups?statistics=true
"lfs_objects_size" : 123,
"job_artifacts_size" : 57,
"packages_size": 0,
- "snippets_size" : 50,
+ "snippets_size" : 50
}
}
]
diff --git a/doc/api/instance_clusters.md b/doc/api/instance_clusters.md
index 99717ba939d..1c4996975f7 100644
--- a/doc/api/instance_clusters.md
+++ b/doc/api/instance_clusters.md
@@ -84,7 +84,7 @@ Example response:
},
"provider_gcp": null,
"management_project": null
- }
+ },
{
"id": 11,
"name": "cluster-3",
diff --git a/doc/api/issue_links.md b/doc/api/issue_links.md
index 25d85577c13..e6e5a0b3d25 100644
--- a/doc/api/issue_links.md
+++ b/doc/api/issue_links.md
@@ -117,7 +117,7 @@ Example response:
"due_date": null,
"web_url": "http://example.com/example/example/issues/11",
"confidential": false,
- "weight": null,
+ "weight": null
},
"target_issue" : {
"id" : 84,
@@ -147,7 +147,7 @@ Example response:
"due_date": null,
"web_url": "http://example.com/example/example/issues/14",
"confidential": false,
- "weight": null,
+ "weight": null
},
"link_type": "relates_to"
}
@@ -198,7 +198,7 @@ DELETE /projects/:id/issues/:issue_iid/links/:issue_link_id
"due_date": null,
"web_url": "http://example.com/example/example/issues/11",
"confidential": false,
- "weight": null,
+ "weight": null
},
"target_issue" : {
"id" : 84,
@@ -228,7 +228,7 @@ DELETE /projects/:id/issues/:issue_iid/links/:issue_link_id
"due_date": null,
"web_url": "http://example.com/example/example/issues/14",
"confidential": false,
- "weight": null,
+ "weight": null
},
"link_type": "relates_to"
}
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 79b1fc78108..a9a3f04a064 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -591,83 +591,79 @@ Example response:
```json
{
- "id" : 1,
- "milestone" : {
- "due_date" : null,
- "project_id" : 4,
- "state" : "closed",
- "description" : "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.",
- "iid" : 3,
- "id" : 11,
- "title" : "v3.0",
- "created_at" : "2016-01-04T15:31:39.788Z",
- "updated_at" : "2016-01-04T15:31:39.788Z",
- "closed_at" : "2016-01-05T15:31:46.176Z"
- },
- "author" : {
- "state" : "active",
- "web_url" : "https://gitlab.example.com/root",
- "avatar_url" : null,
- "username" : "root",
- "id" : 1,
- "name" : "Administrator"
- },
- "description" : "Omnis vero earum sunt corporis dolor et placeat.",
- "state" : "closed",
- "iid" : 1,
- "assignees" : [{
- "avatar_url" : null,
- "web_url" : "https://gitlab.example.com/lennie",
- "state" : "active",
- "username" : "lennie",
- "id" : 9,
- "name" : "Dr. Luella Kovacek"
- }],
- "assignee" : {
- "avatar_url" : null,
- "web_url" : "https://gitlab.example.com/lennie",
- "state" : "active",
- "username" : "lennie",
- "id" : 9,
- "name" : "Dr. Luella Kovacek"
- },
- "labels" : [],
- "upvotes": 4,
- "downvotes": 0,
- "merge_requests_count": 0,
- "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
- "updated_at" : "2016-01-04T15:31:46.176Z",
- "created_at" : "2016-01-04T15:31:46.176Z",
- "closed_at" : null,
- "closed_by" : null,
- "subscribed": false,
- "user_notes_count": 1,
- "due_date": null,
- "web_url": "http://example.com/my-group/my-project/issues/1",
- "references": {
- "short": "#1",
- "relative": "#1",
- "full": "my-group/my-project#1"
- },
- "time_stats": {
- "time_estimate": 0,
- "total_time_spent": 0,
- "human_time_estimate": null,
- "human_total_time_spent": null
- },
- "confidential": false,
- "discussion_locked": false,
- "_links": {
- "self": "http://example.com/api/v4/projects/1/issues/2",
- "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
- "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
- "project": "http://example.com/api/v4/projects/1"
- },
- "task_completion_status":{
- "count":0,
- "completed_count":0
- },
- "weight": null,
+ "id": 1,
+ "milestone": {
+ "due_date": null,
+ "project_id": 4,
+ "state": "closed",
+ "description": "Rerum est voluptatem provident consequuntur molestias similique ipsum dolor.",
+ "iid": 3,
+ "id": 11,
+ "title": "v3.0",
+ "created_at": "2016-01-04T15:31:39.788Z",
+ "updated_at": "2016-01-04T15:31:39.788Z",
+ "closed_at": "2016-01-05T15:31:46.176Z"
+ },
+ "author": {
+ "state": "active",
+ "web_url": "https://gitlab.example.com/root",
+ "avatar_url": null,
+ "username": "root",
+ "id": 1,
+ "name": "Administrator"
+ },
+ "description": "Omnis vero earum sunt corporis dolor et placeat.",
+ "state": "closed",
+ "iid": 1,
+ "assignees": [
+ {
+ "avatar_url": null,
+ "web_url": "https://gitlab.example.com/lennie",
+ "state": "active",
+ "username": "lennie",
+ "id": 9,
+ "name": "Dr. Luella Kovacek"
+ }
+ ],
+ "assignee": {
+ "avatar_url": null,
+ "web_url": "https://gitlab.example.com/lennie",
+ "state": "active",
+ "username": "lennie",
+ "id": 9,
+ "name": "Dr. Luella Kovacek"
+ },
+ "labels": [],
+ "upvotes": 4,
+ "downvotes": 0,
+ "merge_requests_count": 0,
+ "title": "Ut commodi ullam eos dolores perferendis nihil sunt.",
+ "updated_at": "2016-01-04T15:31:46.176Z",
+ "created_at": "2016-01-04T15:31:46.176Z",
+ "closed_at": null,
+ "closed_by": null,
+ "subscribed": false,
+ "user_notes_count": 1,
+ "due_date": null,
+ "web_url": "http://example.com/my-group/my-project/issues/1",
+ "references": {
+ "short": "#1",
+ "relative": "#1",
+ "full": "my-group/my-project#1"
+ },
+ "time_stats": {
+ "time_estimate": 0,
+ "total_time_spent": 0,
+ "human_time_estimate": null,
+ "human_total_time_spent": null
+ },
+ "confidential": false,
+ "discussion_locked": false,
+ "task_completion_status": {
+ "count": 0,
+ "completed_count": 0
+ },
+ "weight": null,
"has_tasks": false,
"_links": {
"self": "http://gitlab.example:3000/api/v4/projects/1/issues/1",
@@ -675,12 +671,6 @@ Example response:
"award_emoji": "http://gitlab.example:3000/api/v4/projects/1/issues/1/award_emoji",
"project": "http://gitlab.example:3000/api/v4/projects/1"
},
- "references": {
- "short": "#1",
- "relative": "#1",
- "full": "gitlab-org/gitlab-test#1"
- },
- "subscribed": true,
"moved_to_id": null,
"service_desk_reply_to": "service.desk@gitlab.com",
"epic_iid": null,
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 97a155bce06..375d52c3c58 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -68,7 +68,6 @@ Example of response
"status": "pending"
},
"ref": "master",
- "artifacts": [],
"runner": null,
"stage": "test",
"status": "failed",
diff --git a/doc/api/keys.md b/doc/api/keys.md
index 98159bcf027..99c9745d8f2 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -31,7 +31,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
"title": "Sample key 25",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1256k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"created_at": "2015-09-03T07:24:44.627Z",
- "expires_at": "2020-05-05T00:00:00.000Z"
+ "expires_at": "2020-05-05T00:00:00.000Z",
"user": {
"name": "John Smith",
"username": "john_smith",
@@ -59,7 +59,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
"identities": [],
"can_create_group": true,
"can_create_project": true,
- "two_factor_enabled": false
+ "two_factor_enabled": false,
"external": false,
"private_profile": null
}
@@ -100,7 +100,7 @@ Example response:
"title": "Sample key 1",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1016k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
"created_at": "2019-11-14T15:11:13.222Z",
- "expires_at": "2020-05-05T00:00:00.000Z"
+ "expires_at": "2020-05-05T00:00:00.000Z",
"user": {
"id": 1,
"name": "Administrator",
diff --git a/doc/api/license.md b/doc/api/license.md
index ceaa471c25f..b69a5e81ea6 100644
--- a/doc/api/license.md
+++ b/doc/api/license.md
@@ -80,7 +80,7 @@ GET /licenses
"Name": "Doe John"
},
"add_ons": {
- "GitLab_FileLocks": 1,
+ "GitLab_FileLocks": 1
}
}
]
diff --git a/doc/api/members.md b/doc/api/members.md
index adfe2df8f30..4c740247a70 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -136,7 +136,7 @@ Example response:
"avatar_url": "https://www.gravatar.com/avatar/c2525a7f58ae3776070e44c106c48e15?s=80&d=identicon",
"web_url": "http://192.168.1.8:3000/root",
"expires_at": "2012-10-22T14:13:35Z",
- "access_level": 30
+ "access_level": 30,
"email": "john@example.com",
"group_saml_identity": {
"extern_uid":"ABC-1234567890",
diff --git a/doc/api/merge_request_approvals.md b/doc/api/merge_request_approvals.md
index 8f2a6bd3d00..6625742e312 100644
--- a/doc/api/merge_request_approvals.md
+++ b/doc/api/merge_request_approvals.md
@@ -664,7 +664,7 @@ GET /projects/:id/merge_requests/:merge_request_iid/approvals
"web_url": "http://localhost:3000/root"
}
}
- ],
+ ]
}
```
@@ -1109,7 +1109,7 @@ does not match, the response code is `409`.
"web_url": "http://localhost:3000/ryley"
}
}
- ],
+ ]
}
```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index d28c7d8e8a7..fcfcc58231b 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -2319,7 +2319,7 @@ Example response:
"short": "!1",
"relative": "!1",
"full": "my-group/my-project!1"
- },
+ }
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7",
"body": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md
index 037b9053c3d..298c0ead8c1 100644
--- a/doc/api/notification_settings.md
+++ b/doc/api/notification_settings.md
@@ -176,7 +176,9 @@ Example responses:
{
"level": "watch"
}
+```
+```json
{
"level": "custom",
"events": {
diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md
index 67529adee93..6b3b6f4f36b 100644
--- a/doc/api/pipeline_schedules.md
+++ b/doc/api/pipeline_schedules.md
@@ -370,8 +370,8 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --form "value=u
```json
{
"key": "NEW_VARIABLE",
- "value": "updated value"
- "variable_type": "env_var",
+ "value": "updated value",
+ "variable_type": "env_var"
}
```
diff --git a/doc/api/pipelines.md b/doc/api/pipelines.md
index 44129b44173..497c70b19ba 100644
--- a/doc/api/pipelines.md
+++ b/doc/api/pipelines.md
@@ -60,7 +60,7 @@ Example of response
"sha": "a91957a858320c0e17f3a0eca7cfacbff50ea29a",
"web_url": "https://example.com/foo/bar/pipelines/47",
"created_at": "2016-08-11T11:28:34.085Z",
- "updated_at": "2016-08-11T11:32:35.169Z",
+ "updated_at": "2016-08-11T11:32:35.169Z"
},
{
"id": 48,
@@ -70,7 +70,7 @@ Example of response
"sha": "eb94b618fb5865b26e80fdd8ae531b7a63ad851a",
"web_url": "https://example.com/foo/bar/pipelines/48",
"created_at": "2016-08-12T10:06:04.561Z",
- "updated_at": "2016-08-12T10:09:56.223Z",
+ "updated_at": "2016-08-12T10:09:56.223Z"
}
]
```
diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md
index 58f5b000958..a17f7d15e76 100644
--- a/doc/api/project_badges.md
+++ b/doc/api/project_badges.md
@@ -59,7 +59,7 @@ Example response:
"rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
"rendered_image_url": "https://shields.io/my/badge",
"kind": "group"
- },
+ }
]
```
@@ -202,6 +202,6 @@ Example response:
"link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
"image_url": "https://shields.io/my/badge",
"rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
- "rendered_image_url": "https://shields.io/my/badge",
+ "rendered_image_url": "https://shields.io/my/badge"
}
```
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index c895a7c4155..b95f8c2984d 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -104,7 +104,7 @@ an email notifying the user to download the file, uploading the exported file to
"export_status": "finished",
"_links": {
"api_url": "https://gitlab.example.com/api/v4/projects/1/export/download",
- "web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/download_export",
+ "web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/download_export"
}
}
```
diff --git a/doc/api/project_repository_storage_moves.md b/doc/api/project_repository_storage_moves.md
index 94aeb665c7f..dd8954f2f0f 100644
--- a/doc/api/project_repository_storage_moves.md
+++ b/doc/api/project_repository_storage_moves.md
@@ -68,6 +68,7 @@ Example response:
"path": "project1",
"path_with_namespace": "namespace1/project1",
"created_at": "2020-05-07T04:27:17.016Z"
+ }
}
]
```
@@ -111,6 +112,7 @@ Example response:
"path": "project1",
"path_with_namespace": "namespace1/project1",
"created_at": "2020-05-07T04:27:17.016Z"
+ }
}
]
```
@@ -150,6 +152,7 @@ Example response:
"path": "project1",
"path_with_namespace": "namespace1/project1",
"created_at": "2020-05-07T04:27:17.016Z"
+ }
}
```
@@ -189,6 +192,7 @@ Example response:
"path": "project1",
"path_with_namespace": "namespace1/project1",
"created_at": "2020-05-07T04:27:17.016Z"
+ }
}
```
@@ -237,6 +241,7 @@ Example response:
"path": "project1",
"path_with_namespace": "namespace1/project1",
"created_at": "2020-05-07T04:27:17.016Z"
+ }
}
```
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index fc8882be283..070429eafd5 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -114,7 +114,7 @@ curl --request POST "https://gitlab.com/api/v4/projects/:id/snippets" \
"files": [
{
"file_path": "example.txt",
- "content" : "source code \n with multiple lines\n",
+ "content" : "source code \n with multiple lines\n"
}
]
}
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 2961cb725b3..db8694d80a1 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -94,7 +94,7 @@ When `simple=true` or the user is unauthenticated this returns something like:
"last_activity_at": "2013-09-30T13:46:02Z",
"forks_count": 0,
"avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
- "star_count": 0,
+ "star_count": 0
},
{
"id": 6,
@@ -189,7 +189,7 @@ When the user is authenticated and `simple` is not set this returns something li
"labels": "http://example.com/api/v4/projects/1/labels",
"events": "http://example.com/api/v4/projects/1/events",
"members": "http://example.com/api/v4/projects/1/members"
- },
+ }
},
{
"id": 6,
@@ -902,7 +902,6 @@ GET /projects/:id
"merge_method": "merge",
"auto_devops_enabled": true,
"auto_devops_deploy_strategy": "continuous",
- "repository_storage": "default",
"approvals_before_merge": 0,
"mirror": false,
"mirror_user_id": 45,
@@ -986,7 +985,7 @@ If the project is a fork, and you provide a valid token to authenticate, the
"name": "MIT License",
"nickname": null,
"html_url": "http://choosealicense.com/licenses/mit/",
- "source_url": "https://opensource.org/licenses/MIT",
+ "source_url": "https://opensource.org/licenses/MIT"
},
"star_count":3812,
"forks_count":3561,
@@ -1661,26 +1660,26 @@ Example responses:
[
{
"starred_since": "2019-01-28T14:47:30.642Z",
- "user":
- {
+ "user": {
"id": 1,
"username": "jane_smith",
"name": "Jane Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
"web_url": "http://localhost:3000/jane_smith"
- }
+ }
},
+ {
"starred_since": "2018-01-02T11:40:26.570Z",
- "user":
- {
- "id": 2,
- "username": "janine_smith",
- "name": "Janine Smith",
- "state": "blocked",
- "avatar_url": "http://gravatar.com/../e32131cd8.jpeg",
- "web_url": "http://localhost:3000/janine_smith"
- }
+ "user": {
+ "id": 2,
+ "username": "janine_smith",
+ "name": "Janine Smith",
+ "state": "blocked",
+ "avatar_url": "http://gravatar.com/../e32131cd8.jpeg",
+ "web_url": "http://localhost:3000/janine_smith"
+ }
+ }
]
```
diff --git a/doc/api/releases/index.md b/doc/api/releases/index.md
index ff66f41b4e8..e4942b34a49 100644
--- a/doc/api/releases/index.md
+++ b/doc/api/releases/index.md
@@ -144,9 +144,9 @@ Example response:
},
"evidences":[
{
- sha: "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
- filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.2/evidence.json",
- collected_at: "2019-01-03T01:56:19.539Z"
+ "sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
+ "filepath": "https://gitlab.example.com/root/awesome-app/-/releases/v0.2/evidence.json",
+ "collected_at": "2019-01-03T01:56:19.539Z"
}
]
},
@@ -208,9 +208,9 @@ Example response:
},
"evidences":[
{
- sha: "c3ffedec13af470e760d6cdfb08790f71cf52c6cde4d",
- filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json",
- collected_at: "2019-01-03T01:55:18.203Z"
+ "sha": "c3ffedec13af470e760d6cdfb08790f71cf52c6cde4d",
+ "filepath": "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json",
+ "collected_at": "2019-01-03T01:55:18.203Z"
}
]
}
@@ -340,9 +340,9 @@ Example response:
},
"evidences":[
{
- sha: "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
- filepath: "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json",
- collected_at: "2019-07-16T14:00:12.256Z"
+ "sha": "760d6cdfb0879c3ffedec13af470e0f71cf52c6cde4d",
+ "filepath": "https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json",
+ "collected_at": "2019-07-16T14:00:12.256Z"
}
]
}
@@ -482,7 +482,7 @@ Example response:
}
],
"evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.3/evidence.json"
- },
+ }
}
```
@@ -625,7 +625,7 @@ Example response:
],
"evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json"
- },
+ }
}
```
@@ -709,7 +709,7 @@ Example response:
],
"evidence_file_path":"https://gitlab.example.com/root/awesome-app/-/releases/v0.1/evidence.json"
- },
+ }
}
```
diff --git a/doc/api/runners.md b/doc/api/runners.md
index f9febaef305..1f0209c3cae 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -126,7 +126,7 @@ Example response:
"ip_address": "127.0.0.1",
"is_shared": true,
"name": null,
- "online": false
+ "online": false,
"status": "offline"
},
{
@@ -136,7 +136,7 @@ Example response:
"ip_address": "127.0.0.1",
"is_shared": false,
"name": null,
- "online": true
+ "online": true,
"status": "paused"
},
{
@@ -428,7 +428,7 @@ Example response:
"ip_address": "127.0.0.1",
"is_shared": true,
"name": null,
- "online": true
+ "online": true,
"status": "paused"
}
]
diff --git a/doc/api/services.md b/doc/api/services.md
index d42752848c5..27e057081bc 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -42,7 +42,7 @@ Example response:
"wiki_page_events": true,
"job_events": true,
"comment_on_event_enabled": true
- }
+ },
{
"id": 76,
"title": "Alerts endpoint",
diff --git a/doc/api/users.md b/doc/api/users.md
index 0e4012935f9..228194f10fa 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -1762,6 +1762,6 @@ Example response:
"source_name": "Group three",
"source_type": "Namespace",
"access_level": "20"
- },
+ }
]
```
diff --git a/doc/ci/environments/deployment_safety.md b/doc/ci/environments/deployment_safety.md
index e38d9031ffd..6fda6bb0d8b 100644
--- a/doc/ci/environments/deployment_safety.md
+++ b/doc/ci/environments/deployment_safety.md
@@ -154,7 +154,7 @@ If you have multiple jobs for the same environment (including non-deployment job
build:service-a:
environment:
name: production
-
+
build:service-b:
environment:
name: production
diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md
index d4b9282ac38..874a83f94d9 100644
--- a/doc/ci/troubleshooting.md
+++ b/doc/ci/troubleshooting.md
@@ -269,7 +269,7 @@ which pipelines can run.
resource_group = Project.find_by_full_path('...').resource_groups.find_by(key: 'the-group-name')
busy_resources = resource_group.resources.where('build_id IS NOT NULL')
-# identify which builds are occupying the resource
+# identify which builds are occupying the resource
# (I think it should be 1 as of today)
busy_resources.pluck(:build_id)
diff --git a/doc/development/cascading_settings.md b/doc/development/cascading_settings.md
index 0233e9f9c3c..2d2ed6e8b1b 100644
--- a/doc/development/cascading_settings.md
+++ b/doc/development/cascading_settings.md
@@ -26,7 +26,7 @@ Settings are not cascading by default. To define a cascading setting, take the f
```ruby
class NamespaceSetting
include CascadingNamespaceSettingAttribute
-
+
cascading_attr :delayed_project_removal
end
```
@@ -40,11 +40,11 @@ Settings are not cascading by default. To define a cascading setting, take the f
```ruby
class AddDelayedProjectRemovalCascadingSetting < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers::CascadingNamespaceSettings
-
+
def up
add_cascading_namespace_setting :delayed_project_removal, :boolean, default: false, null: false
end
-
+
def down
remove_cascading_namespace_setting :delayed_project_removal
end
@@ -100,7 +100,7 @@ cascaded value using the following criteria:
### `_locked?` method
By default, the `_locked?` method (`delayed_project_removal_locked?`) returns
-`true` if an ancestor of the group or application setting locks the attribute.
+`true` if an ancestor of the group or application setting locks the attribute.
It returns `false` when called from the group that locked the attribute.
When `include_self: true` is specified, it returns `true` when called from the group that locked the attribute.
diff --git a/doc/development/gemfile.md b/doc/development/gemfile.md
index f50e19bc383..fcb317e1e88 100644
--- a/doc/development/gemfile.md
+++ b/doc/development/gemfile.md
@@ -19,6 +19,63 @@ dependencies and build times.
Refer to [licensing guidelines](licensing.md) for ensuring license compliance.
+## GitLab-created gems
+
+Sometimes we create libraries within our codebase that we want to
+extract, either because we want to use them in other applications
+ourselves, or because we think it would benefit the wider community.
+Extracting code to a gem also means that we can be sure that the gem
+does not contain any hidden dependencies on our application code.
+
+In general, we want to think carefully before doing this as there are
+also disadvantages:
+
+1. Gems - even those maintained by GitLab - do not necessarily go
+ through the same [code review process](code_review.md) as the main
+ Rails application.
+1. Extracting the code into a separate project means that we need a
+ minimum of two merge requests to change functionality: one in the gem
+ to make the functional change, and one in the Rails app to bump the
+ version.
+1. Our needs for our own usage of the gem may not align with the wider
+ community's needs. In general, if we are not using the latest version
+ of our own gem, that might be a warning sign.
+
+In the case where we do want to extract some library code we've written
+to a gem, go through these steps:
+
+1. Start with the code in the Rails application. Here it's fine to have
+ the code in `lib/` and loaded automatically. We can skip this step if
+ the step below makes more sense initially.
+1. Before extracting to its own project, move the gem to `vendor/gems` and
+ load it in the `Gemfile` using the `path` option. This gives us a gem
+ that can be published to RubyGems.org, with its own test suite and
+ isolated set of dependencies, that is still in our main code tree and
+ goes through the standard code review process.
+ - For an example, see the [merge request !57805](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57805).
+1. Once the gem is stable - we have been using it in production for a
+ while with few, if any, changes - extract to its own project under
+ the `gitlab-org` namespace.
+ 1. When creating the project, follow the [instructions for new projects](https://about.gitlab.com/handbook/engineering/#creating-a-new-project).
+ 1. Follow the instructions for setting up a [CI/CD configuration](https://about.gitlab.com/handbook/engineering/#cicd-configuration).
+ 1. Follow the instructions for [publishing a project](https://about.gitlab.com/handbook/engineering/#publishing-a-project).
+ - See [issue
+ #325463](https://gitlab.com/gitlab-org/gitlab/-/issues/325463)
+ for an example.
+ - In some cases we may want to move a gem to its own namespace. Some
+ examples might be that it will naturally have more than one project
+ (say, something that has plugins as separate libraries), or that we
+ expect non-GitLab-team-members to be maintainers on this project as
+ well as GitLab team members.
+
+ The latter situation (maintainers from outside GitLab) could also
+ apply if someone who currently works at GitLab wants to maintain
+ the gem beyond their time working at GitLab.
+
+When publishing a gem to RubyGems.org, also note the section on [gem
+owners](https://about.gitlab.com/handbook/developer-onboarding/#ruby-gems)
+in the handbook.
+
## Upgrade Rails
When upgrading the Rails gem and its dependencies, you also should update the following:
diff --git a/doc/development/merge_request_performance_guidelines.md b/doc/development/merge_request_performance_guidelines.md
index 0f696d39092..6ce372ebc0d 100644
--- a/doc/development/merge_request_performance_guidelines.md
+++ b/doc/development/merge_request_performance_guidelines.md
@@ -162,7 +162,22 @@ query. This in turn makes it much harder for this code to overload a database.
In a DB cluster we have many read replicas and one primary. A classic use of scaling the DB is to have read-only actions be performed by the replicas. We use [load balancing](../administration/database_load_balancing.md) to distribute this load. This allows for the replicas to grow as the pressure on the DB grows.
-By default, queries use read-only replicas, but due to [primary sticking](../administration/database_load_balancing.md#primary-sticking), GitLab sticks to using the primary for a certain period of time and reverts back to secondaries after they have either caught up or after 30 seconds, which can lead to a considerable amount of unnecessary load on the primary. In this [merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56849) we introduced the `without_sticky_writes` block to prevent switching to the primary. This [merge request example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57328) provides a good use case for when queries can stick to the primary and how to prevent this by using `without_sticky_writes`.
+By default, queries use read-only replicas, but due to
+[primary sticking](../administration/database_load_balancing.md#primary-sticking), GitLab uses the
+primary for some time and reverts to secondaries after they have either caught up or after 30 seconds.
+Doing this can lead to a considerable amount of unnecessary load on the primary.
+To prevent switching to the primary [merge request 56849](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56849) introduced the
+`without_sticky_writes` block. Typically, this method can be applied to prevent primary stickiness
+after a trivial or insignificant write which doesn't affect the following queries in the same session.
+
+To learn when a usage timestamp update can lead the session to stick to the primary and how to
+prevent it by using `without_sticky_writes`, see [merge request 57328](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57328)
+
+As a counterpart of the `without_sticky_writes` utility,
+[merge request 59167](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59167) introduced
+`use_replicas_for_read_queries`. This method forces all read-only queries inside its block to read
+replicas regardless of the current primary stickiness.
+This utility is reserved for cases where queries can tolerate replication lag.
Internally, our database load balancer classifies the queries based on their main statement (`select`, `update`, `delete`, etc.). When in doubt, it redirects the queries to the primary database. Hence, there are some common cases the load balancer sends the queries to the primary unnecessarily:
@@ -171,7 +186,12 @@ Internally, our database load balancer classifies the queries based on their mai
- In-flight connection configuration set
- Sidekiq background jobs
-Worse, after the above queries are executed, GitLab [sticks to the primary](../administration/database_load_balancing.md#primary-sticking). In [this merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56476), we introduced `use_replica_if_possible` to make the inside queries prefer to use the replicas. That MR is also an example how we redirected a costly, time-consuming query to the replicas.
+After the above queries are executed, GitLab
+[sticks to the primary](../administration/database_load_balancing.md#primary-sticking).
+To make the inside queries prefer using the replicas,
+[merge request 59086](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59086) introduced
+`fallback_to_replicas_for_ambiguous_queries`. This MR is also an example of how we redirected a
+costly, time-consuming query to the replicas.
## Use CTEs wisely
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index 41b5d3179ed..51724d32f3a 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -451,7 +451,7 @@ expect(page).to have_current_path 'gitlab/gitlab-test/-/issues'
expect(page).to have_title 'Not Found'
-# acceptable when a more specific matcher above is not possible
+# acceptable when a more specific matcher above is not possible
expect(page).to have_css 'h2', text: 'Issue title'
expect(page).to have_css 'p', text: 'Issue description', exact: true
expect(page).to have_css '[data-testid="weight"]', text: 2
diff --git a/doc/development/usage_ping/index.md b/doc/development/usage_ping/index.md
index 09c388eb115..0e12c620a8a 100644
--- a/doc/development/usage_ping/index.md
+++ b/doc/development/usage_ping/index.md
@@ -248,12 +248,12 @@ To deprecate a metric:
end
end
```
-
+
1. Update the Metrics Dictionary following [guidelines instructions](dictionary.md).
### 4. Remove a metric
-Only deprecated metrics can be removed from Usage Ping.
+Only deprecated metrics can be removed from Usage Ping.
For an example of the metric removal process take a look at this [example issue](https://gitlab.com/gitlab-org/gitlab/-/issues/297029)
@@ -262,9 +262,9 @@ To remove a deprecated metric:
1. Verify that removing the metric from the Usage Ping payload does not cause
errors in [Version App](https://gitlab.com/gitlab-services/version-gitlab-com)
when the updated payload is collected and processed. Version App collects
- and persists all Usage Ping reports. To do that you can modify
+ and persists all Usage Ping reports. To do that you can modify
[fixtures](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/master/spec/support/usage_data_helpers.rb#L540)
- used to test
+ used to test
[`UsageDataController#create`](https://gitlab.com/gitlab-services/version-gitlab-com/-/blob/3760ef28/spec/controllers/usage_data_controller_spec.rb#L75)
endpoint, and assure that test suite does not fail when metric that you wish to remove is not included into test payload.
@@ -273,7 +273,7 @@ To remove a deprecated metric:
Ask for confirmation that the metric is not referred to in any SiSense dashboards and
can be safely removed from Usage Ping. Use this
[example issue](https://gitlab.com/gitlab-data/analytics/-/issues/7539) for guidance.
- This step can be skipped if verification done during [deprecation process](#3-deprecate-a-metric)
+ This step can be skipped if verification done during [deprecation process](#3-deprecate-a-metric)
reported that metric is not required by any data transformation in Snowflake data warehouse nor it is
used by any of SiSense dashboards.
@@ -288,15 +288,15 @@ To remove a deprecated metric:
instances might not immediately update to the latest version of GitLab, and
therefore continue to report the removed metric. The Product Intelligence team
requires a record of all removed metrics in order to identify and filter them.
-
+
For example please take a look at this [merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60149/diffs#b01f429a54843feb22265100c0e4fec1b7da1240_10_10).
-
+
1. After you verify the metric can be safely removed,
remove the metric's instrumentation from
[`lib/gitlab/usage_data.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data.rb)
or
[`ee/lib/ee/gitlab/usage_data.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/lib/ee/gitlab/usage_data.rb).
-
+
For example please take a look at this [merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60149/diffs#6335dc533bd21df26db9de90a02dd66278c2390d_167_167).
1. Remove any other records related to the metric:
diff --git a/doc/install/azure/index.md b/doc/install/azure/index.md
index 100bb71cc8c..2fca70fd07a 100644
--- a/doc/install/azure/index.md
+++ b/doc/install/azure/index.md
@@ -190,7 +190,7 @@ To set up the GitLab external URL:
1. Open `/etc/gitlab/gitlab.rb` with your editor.
1. Find `external_url` and replace it with your own domain name. For the sake
of this example, use the default domain name Azure sets up.
- Using `https` in the URL
+ Using `https` in the URL
[automatically enables](https://docs.gitlab.com/omnibus/settings/ssl.html#lets-encrypt-integration),
Let's Encrypt, and sets HTTPS by default:
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 468d068725d..da1278b9edd 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -442,7 +442,7 @@ in the OmniAuth [`info` hash](https://github.com/omniauth/omniauth/wiki/Auth-Has
For example, if your SAMLResponse contains an Attribute called `EmailAddress`,
specify `{ email: ['EmailAddress'] }` to map the Attribute to the
-corresponding key in the `info` hash. URI-named Attributes are also supported, for example,
+corresponding key in the `info` hash. URI-named Attributes are also supported, for example,
`{ email: ['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] }`.
This setting allows you tell GitLab where to look for certain attributes required
@@ -859,7 +859,7 @@ For this you need take the following into account:
the request to contain one. In this case the fingerprint or fingerprint
validators are optional
-If none of the above described scenarios is valid, the request
+If none of the above described scenarios is valid, the request
fails with one of the mentioned errors.
### User is blocked when signing in through SAML
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index b649ae3430f..358323e4ef5 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -325,7 +325,7 @@ If you are using [EGit](https://www.eclipse.org/egit/), you can [add your SSH ke
If you're running Windows 10, you can either use the [Windows Subsystem for Linux (WSL)](https://docs.microsoft.com/en-us/windows/wsl/install-win10)
with [WSL 2](https://docs.microsoft.com/en-us/windows/wsl/install-win10#update-to-wsl-2) which
-has both `git` and `ssh` preinstalled, or install [Git for Windows](https://gitforwindows.org) to
+has both `git` and `ssh` preinstalled, or install [Git for Windows](https://gitforwindows.org) to
use SSH through Powershell.
The SSH key generated in WSL is not directly available for Git for Windows, and vice versa,
diff --git a/doc/user/clusters/agent/index.md b/doc/user/clusters/agent/index.md
index d4551edddcd..4978e46cccf 100644
--- a/doc/user/clusters/agent/index.md
+++ b/doc/user/clusters/agent/index.md
@@ -100,7 +100,7 @@ To use the KAS:
### Define a configuration repository
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in GitLab 13.7, the Agent manifest configuration can be added to multiple directories (or subdirectories) of its repository.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259669) in GitLab 13.7, the Agent manifest configuration can be added to multiple directories (or subdirectories) of its repository.
To configure an Agent, you need:
diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md
index 8e3718f3255..c5f9d3dd47b 100644
--- a/doc/user/packages/npm_registry/index.md
+++ b/doc/user/packages/npm_registry/index.md
@@ -318,7 +318,7 @@ npmScopes:
foo:
npmRegistryServer: "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/"
npmPublishRegistry: "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/"
-
+
npmRegistries:
//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:
npmAlwaysAuth: true
diff --git a/doc/user/project/import/svn.md b/doc/user/project/import/svn.md
index 712bb3cc954..8be69cdeb79 100644
--- a/doc/user/project/import/svn.md
+++ b/doc/user/project/import/svn.md
@@ -44,9 +44,9 @@ directly in a file system level.
The first step to mirror you SVN repository in GitLab is to create a new empty
project that is used as a mirror. For Omnibus installations the path to
-the repository is
+the repository is
`/var/opt/gitlab/git-data/repositories/USER/REPO.git` by default. For
-installations from source, the default repository directory is
+installations from source, the default repository directory is
`/home/git/repositories/USER/REPO.git`. For convenience, assign this path to a
variable:
diff --git a/lib/gitlab/database/background_migration/batch_optimizer.rb b/lib/gitlab/database/background_migration/batch_optimizer.rb
index 033dd5d54e1..7523c828399 100644
--- a/lib/gitlab/database/background_migration/batch_optimizer.rb
+++ b/lib/gitlab/database/background_migration/batch_optimizer.rb
@@ -17,22 +17,26 @@ module Gitlab
class BatchOptimizer
# Target time efficiency for a job
# Time efficiency is defined as: job duration / interval
- TARGET_EFFICIENCY = (0.8..0.98).freeze
+ TARGET_EFFICIENCY = (0.9..0.95).freeze
# Lower and upper bound for the batch size
- ALLOWED_BATCH_SIZE = (1_000..1_000_000).freeze
+ ALLOWED_BATCH_SIZE = (1_000..2_000_000).freeze
- # Use this batch_size multiplier to increase batch size
- INCREASE_MULTIPLIER = 1.1
+ # Limit for the multiplier of the batch size
+ MAX_MULTIPLIER = 1.2
- # Use this batch_size multiplier to decrease batch size
- DECREASE_MULTIPLIER = 0.8
+ # When smoothing time efficiency, use this many jobs
+ NUMBER_OF_JOBS = 20
- attr_reader :migration, :number_of_jobs
+ # Smoothing factor for exponential moving average
+ EMA_ALPHA = 0.4
- def initialize(migration, number_of_jobs: 10)
+ attr_reader :migration, :number_of_jobs, :ema_alpha
+
+ def initialize(migration, number_of_jobs: NUMBER_OF_JOBS, ema_alpha: EMA_ALPHA)
@migration = migration
@number_of_jobs = number_of_jobs
+ @ema_alpha = ema_alpha
end
def optimize!
@@ -47,20 +51,15 @@ module Gitlab
private
def batch_size_multiplier
- efficiency = migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs)
+ efficiency = migration.smoothed_time_efficiency(number_of_jobs: number_of_jobs, alpha: ema_alpha)
- return unless efficiency
+ return if efficiency.nil? || efficiency == 0
- if TARGET_EFFICIENCY.include?(efficiency)
- # We hit the range - no change
- nil
- elsif efficiency > TARGET_EFFICIENCY.max
- # We're above the range - decrease by 20%
- DECREASE_MULTIPLIER
- else
- # We're below the range - increase by 10%
- INCREASE_MULTIPLIER
- end
+ # We hit the range - no change
+ return if TARGET_EFFICIENCY.include?(efficiency)
+
+ # Assumption: time efficiency is linear in the batch size
+ [TARGET_EFFICIENCY.max / efficiency, MAX_MULTIPLIER].min
end
end
end
diff --git a/lib/gitlab/usage_data_counters/editor_unique_counter.rb b/lib/gitlab/usage_data_counters/editor_unique_counter.rb
index bef3fc7b504..bc0126cd893 100644
--- a/lib/gitlab/usage_data_counters/editor_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/editor_unique_counter.rb
@@ -50,7 +50,6 @@ module Gitlab
private
def track_unique_action(action, author, time)
- return unless Feature.enabled?(:track_editor_edit_actions, default_enabled: true)
return unless author
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(action, values: author.id, time: time)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 00c749e85d5..6c71d4b9807 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8287,6 +8287,9 @@ msgstr ""
msgid "ComplianceFramework|This project is regulated by %{framework}."
msgstr ""
+msgid "Component"
+msgstr ""
+
msgid "Confidence"
msgstr ""
@@ -10354,6 +10357,9 @@ msgstr ""
msgid "Data is still calculating..."
msgstr ""
+msgid "Data type"
+msgstr ""
+
msgid "Database update failed"
msgstr ""
@@ -14436,6 +14442,9 @@ msgstr ""
msgid "GeoNodes|secondary nodes"
msgstr ""
+msgid "Geo|%{component} synced"
+msgstr ""
+
msgid "Geo|%{itemTitle} checksum progress"
msgstr ""
@@ -14655,16 +14664,13 @@ msgstr ""
msgid "Geo|Removing a Geo secondary node stops the synchronization to that node. Are you sure?"
msgstr ""
-msgid "Geo|Replicated data is verified with the secondary node(s) using checksums."
-msgstr ""
-
-msgid "Geo|Replication Details"
+msgid "Geo|Replicated data is verified with the secondary node(s) using checksums"
msgstr ""
-msgid "Geo|Replication Details Desktop"
+msgid "Geo|Replicated data is verified with the secondary node(s) using checksums."
msgstr ""
-msgid "Geo|Replication Details Mobile"
+msgid "Geo|Replication Details"
msgstr ""
msgid "Geo|Replication details"
@@ -14733,6 +14739,9 @@ msgstr ""
msgid "Geo|Synchronization settings"
msgstr ""
+msgid "Geo|Synchronization status"
+msgstr ""
+
msgid "Geo|The database is currently %{db_lag} behind the primary node."
msgstr ""
@@ -14778,6 +14787,9 @@ msgstr ""
msgid "Geo|Verification failed - %{error}"
msgstr ""
+msgid "Geo|Verification status"
+msgstr ""
+
msgid "Geo|Verificaton information"
msgstr ""
@@ -32177,6 +32189,9 @@ msgstr ""
msgid "There was a problem fetching groups."
msgstr ""
+msgid "There was a problem fetching iterations."
+msgstr ""
+
msgid "There was a problem fetching labels."
msgstr ""
@@ -35841,7 +35856,7 @@ msgstr ""
msgid "Webhooks|URL is triggered by a push to the repository"
msgstr ""
-msgid "Webhooks|URL is triggered when a confidential issue is created, updated, or merged"
+msgid "Webhooks|URL is triggered when a confidential issue is created, updated, closed, or reopened"
msgstr ""
msgid "Webhooks|URL is triggered when a deployment starts, finishes, fails, or is canceled"
@@ -35868,7 +35883,7 @@ msgstr ""
msgid "Webhooks|URL is triggered when a wiki page is created or updated"
msgstr ""
-msgid "Webhooks|URL is triggered when an issue is created, updated, or merged"
+msgid "Webhooks|URL is triggered when an issue is created, updated, closed, or reopened"
msgstr ""
msgid "Webhooks|URL is triggered when someone adds a comment"
diff --git a/package.json b/package.json
index 7f2d7fb342e..c669c6865c8 100644
--- a/package.json
+++ b/package.json
@@ -113,7 +113,7 @@
"deckar01-task_list": "^2.3.1",
"diff": "^3.4.0",
"document-register-element": "1.14.3",
- "dompurify": "^2.2.7",
+ "dompurify": "^2.2.8",
"dropzone": "^4.2.0",
"editorconfig": "^0.15.3",
"emoji-regex": "^7.0.3",
diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb
index 5980246944e..be4b6d6b82d 100644
--- a/spec/features/projects/settings/user_manages_project_members_spec.rb
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -52,9 +52,11 @@ RSpec.describe 'Projects > Settings > User manages project members' do
end
describe 'when the :invite_members_group_modal is disabled' do
- it 'imports a team from another project', :js do
+ before do
stub_feature_flags(invite_members_group_modal: false)
+ end
+ it 'imports a team from another project', :js do
project2.add_maintainer(user)
project2.add_reporter(user_mike)
diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js
index faeab0c244d..f75c3d8bcb9 100644
--- a/spec/frontend/issues_list/mock_data.js
+++ b/spec/frontend/issues_list/mock_data.js
@@ -14,6 +14,10 @@ export const locationSearch = [
'not[label_name][]=drama',
'my_reaction_emoji=thumbsup',
'confidential=no',
+ 'iteration_title=season:+%234',
+ 'not[iteration_title]=season:+%2320',
+ 'weight=1',
+ 'not[weight]=3',
].join('&');
export const filteredTokens = [
@@ -29,6 +33,10 @@ export const filteredTokens = [
{ type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } },
{ type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: 'confidential', value: { data: 'no', operator: OPERATOR_IS } },
+ { type: 'iteration', value: { data: 'season: #4', operator: OPERATOR_IS } },
+ { type: 'iteration', value: { data: 'season: #20', operator: OPERATOR_IS_NOT } },
+ { type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
+ { type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
{ type: 'filtered-search-term', value: { data: 'find' } },
{ type: 'filtered-search-term', value: { data: 'issues' } },
];
@@ -44,6 +52,10 @@ export const apiParams = {
'not[labels]': 'live action,drama',
my_reaction_emoji: 'thumbsup',
confidential: 'no',
+ iteration_title: 'season: #4',
+ 'not[iteration_title]': 'season: #20',
+ weight: '1',
+ 'not[weight]': '3',
};
export const urlParams = {
@@ -57,4 +69,8 @@ export const urlParams = {
'not[label_name][]': ['live action', 'drama'],
my_reaction_emoji: ['thumbsup'],
confidential: ['no'],
+ iteration_title: ['season: #4'],
+ 'not[iteration_title]': ['season: #20'],
+ weight: ['1'],
+ 'not[weight]': ['3'],
};
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index 46b7e49979e..c49a1ab68b1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -5,8 +5,10 @@ import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/auth
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import EpicToken from '~/vue_shared/components/filtered_search_bar/tokens/epic_token.vue';
+import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
export const mockAuthor1 = {
id: 1,
@@ -98,6 +100,15 @@ export const mockAuthorToken = {
fetchAuthors: Api.projectUsers.bind(Api),
};
+export const mockIterationToken = {
+ type: 'iteration',
+ icon: 'iteration',
+ title: 'Iteration',
+ unique: true,
+ token: IterationToken,
+ fetchIterations: () => Promise.resolve(),
+};
+
export const mockLabelToken = {
type: 'label_name',
icon: 'labels',
@@ -155,6 +166,14 @@ export const mockMembershipToken = {
],
};
+export const mockWeightToken = {
+ type: 'weight',
+ icon: 'weight',
+ title: 'Weight',
+ unique: true,
+ token: WeightToken,
+};
+
export const mockMembershipTokenOptionsWithoutTitles = {
...mockMembershipToken,
options: [{ value: 'exclude' }, { value: 'only' }],
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
new file mode 100644
index 00000000000..ca5dc984ae0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/iteration_token_spec.js
@@ -0,0 +1,78 @@
+import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import createFlash from '~/flash';
+import IterationToken from '~/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue';
+import { mockIterationToken } from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('IterationToken', () => {
+ const title = 'gitlab-org: #1';
+ let wrapper;
+
+ const createComponent = ({ config = mockIterationToken, value = { data: '' } } = {}) =>
+ mount(IterationToken, {
+ propsData: {
+ config,
+ value,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: 'custom-class',
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders iteration value', async () => {
+ wrapper = createComponent({ value: { data: title } });
+
+ await wrapper.vm.$nextTick();
+
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // `Iteration` `=` `gitlab-org: #1`
+ expect(tokenSegments.at(2).text()).toBe(title);
+ });
+
+ it('fetches initial values', () => {
+ const fetchIterationsSpy = jest.fn().mockResolvedValue();
+
+ wrapper = createComponent({
+ config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy },
+ value: { data: title },
+ });
+
+ expect(fetchIterationsSpy).toHaveBeenCalledWith(title);
+ });
+
+ it('fetches iterations on user input', () => {
+ const search = 'hello';
+ const fetchIterationsSpy = jest.fn().mockResolvedValue();
+
+ wrapper = createComponent({
+ config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy },
+ });
+
+ wrapper.findComponent(GlFilteredSearchToken).vm.$emit('input', { data: search });
+
+ expect(fetchIterationsSpy).toHaveBeenCalledWith(search);
+ });
+
+ it('renders error message when request fails', async () => {
+ const fetchIterationsSpy = jest.fn().mockRejectedValue();
+
+ wrapper = createComponent({
+ config: { ...mockIterationToken, fetchIterations: fetchIterationsSpy },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ message: 'There was a problem fetching iterations.',
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js
new file mode 100644
index 00000000000..9a72be636cd
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/weight_token_spec.js
@@ -0,0 +1,37 @@
+import { GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
+import { mockWeightToken } from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('WeightToken', () => {
+ const weight = '3';
+ let wrapper;
+
+ const createComponent = ({ config = mockWeightToken, value = { data: '' } } = {}) =>
+ mount(WeightToken, {
+ propsData: {
+ config,
+ value,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ suggestionsListClass: 'custom-class',
+ },
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders weight value', () => {
+ wrapper = createComponent({ value: { data: weight } });
+
+ const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // `Weight` `=` `3`
+ expect(tokenSegments.at(2).text()).toBe(weight);
+ });
+});
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index dbe4f970a99..3edab40ac3f 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -3,6 +3,8 @@
require "spec_helper"
RSpec.describe InviteMembersHelper do
+ include Devise::Test::ControllerHelpers
+
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user, developer_projects: [project]) }
@@ -14,18 +16,19 @@ RSpec.describe InviteMembersHelper do
context 'with project' do
before do
+ allow(helper).to receive(:current_user) { owner }
assign(:project, project)
end
describe "#can_invite_members_for_project?" do
- context 'when the user can_import_members' do
+ context 'when the user can_manage_project_members' do
before do
- allow(helper).to receive(:can_import_members?).and_return(true)
+ allow(helper).to receive(:can_manage_project_members?).and_return(true)
end
it 'returns true' do
expect(helper.can_invite_members_for_project?(project)).to eq true
- expect(helper).to have_received(:can_import_members?)
+ expect(helper).to have_received(:can_manage_project_members?)
end
context 'when feature flag is disabled' do
@@ -35,14 +38,14 @@ RSpec.describe InviteMembersHelper do
it 'returns false' do
expect(helper.can_invite_members_for_project?(project)).to eq false
- expect(helper).not_to have_received(:can_import_members?)
+ expect(helper).not_to have_received(:can_manage_project_members?)
end
end
end
- context 'when the user can not invite members' do
+ context 'when the user can not manage project members' do
before do
- expect(helper).to receive(:can_import_members?).and_return(false)
+ expect(helper).to receive(:can_manage_project_members?).and_return(false)
end
it 'returns false' do
@@ -87,7 +90,7 @@ RSpec.describe InviteMembersHelper do
allow(helper).to receive(:current_user) { user }
end
- context 'when the user can_import_members' do
+ context 'when the user can admin_group_member' do
before do
allow(helper).to receive(:can?).with(user, :admin_group_member, group).and_return(true)
end
@@ -109,7 +112,7 @@ RSpec.describe InviteMembersHelper do
end
end
- context 'when the user can not invite members' do
+ context 'when the user can not admin_group_member' do
before do
expect(helper).to receive(:can?).with(user, :admin_group_member, group).and_return(false)
end
diff --git a/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb b/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb
index 5386e5b0b1d..95863ce3765 100644
--- a/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batch_optimizer_spec.rb
@@ -4,16 +4,19 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigration::BatchOptimizer do
describe '#optimize' do
- subject { described_class.new(migration, number_of_jobs: number_of_jobs).optimize! }
+ subject { described_class.new(migration, number_of_jobs: number_of_jobs, ema_alpha: ema_alpha).optimize! }
let(:migration) { create(:batched_background_migration, batch_size: batch_size, sub_batch_size: 100, interval: 120) }
let(:batch_size) { 10_000 }
let_it_be(:number_of_jobs) { 5 }
+ let_it_be(:ema_alpha) { 0.4 }
+
+ let_it_be(:target_efficiency) { described_class::TARGET_EFFICIENCY.max }
def mock_efficiency(eff)
- expect(migration).to receive(:smoothed_time_efficiency).with(number_of_jobs: number_of_jobs).and_return(eff)
+ expect(migration).to receive(:smoothed_time_efficiency).with(number_of_jobs: number_of_jobs, alpha: ema_alpha).and_return(eff)
end
it 'with unknown time efficiency, it keeps the batch size' do
@@ -34,25 +37,55 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchOptimizer do
expect { subject }.not_to change { migration.reload.batch_size }
end
- it 'with a time efficiency of 70%, it increases the batch size by 10%' do
- mock_efficiency(0.7)
+ it 'with a time efficiency of 85%, it increases the batch size' do
+ time_efficiency = 0.85
+
+ mock_efficiency(time_efficiency)
+
+ new_batch_size = ((target_efficiency / time_efficiency) * batch_size).to_i
+
+ expect { subject }.to change { migration.reload.batch_size }.from(batch_size).to(new_batch_size)
+ end
+
+ it 'with a time efficiency of 110%, it decreases the batch size' do
+ time_efficiency = 1.1
+
+ mock_efficiency(time_efficiency)
+
+ new_batch_size = ((target_efficiency / time_efficiency) * batch_size).to_i
- expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(11_000)
+ expect { subject }.to change { migration.reload.batch_size }.from(batch_size).to(new_batch_size)
end
- it 'with a time efficiency of 110%, it decreases the batch size by 20%' do
- mock_efficiency(1.1)
+ context 'reaching the upper limit for an increase' do
+ it 'caps the batch size multiplier at 20% when increasing' do
+ time_efficiency = 0.1 # this would result in a factor of 10 if not limited
- expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(8_000)
+ mock_efficiency(time_efficiency)
+
+ new_batch_size = (1.2 * batch_size).to_i
+
+ expect { subject }.to change { migration.reload.batch_size }.from(batch_size).to(new_batch_size)
+ end
+
+ it 'does not limit the decrease multiplier' do
+ time_efficiency = 10
+
+ mock_efficiency(time_efficiency)
+
+ new_batch_size = (0.1 * batch_size).to_i
+
+ expect { subject }.to change { migration.reload.batch_size }.from(batch_size).to(new_batch_size)
+ end
end
context 'reaching the upper limit for the batch size' do
- let(:batch_size) { 950_000 }
+ let(:batch_size) { 1_950_000 }
it 'caps the batch size at 10M' do
mock_efficiency(0.7)
- expect { subject }.to change { migration.reload.batch_size }.to(1_000_000)
+ expect { subject }.to change { migration.reload.batch_size }.to(2_000_000)
end
end
diff --git a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
index 82db3d94493..5f66387c82b 100644
--- a/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/editor_unique_counter_spec.rb
@@ -28,14 +28,6 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red
it 'does not track edit actions if author is not present' do
expect(track_action(author: nil)).to be_nil
end
-
- context 'when feature flag track_editor_edit_actions is disabled' do
- it 'does not track edit actions' do
- stub_feature_flags(track_editor_edit_actions: false)
-
- expect(track_action(author: user1)).to be_nil
- end
- end
end
context 'for web IDE edit actions' do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 9969c0775d7..7c9e7986aab 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -224,6 +224,41 @@ RSpec.describe Namespace do
it { expect(namespace.human_name).to eq(namespace.owner_name) }
end
+ describe '#any_project_has_container_registry_tags?' do
+ subject { namespace.any_project_has_container_registry_tags? }
+
+ let!(:project_without_registry) { create(:project, namespace: namespace) }
+
+ context 'without tags' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with tags' do
+ before do
+ repositories = create_list(:container_repository, 3)
+ create(:project, namespace: namespace, container_repositories: repositories)
+
+ stub_container_registry_config(enabled: true)
+ end
+
+ it 'finds tags' do
+ stub_container_registry_tags(repository: :any, tags: ['tag'])
+
+ is_expected.to be_truthy
+ end
+
+ it 'does not cause N+1 query in fetching registries' do
+ stub_container_registry_tags(repository: :any, tags: [])
+ control_count = ActiveRecord::QueryRecorder.new { namespace.any_project_has_container_registry_tags? }.count
+
+ other_repositories = create_list(:container_repository, 2)
+ create(:project, namespace: namespace, container_repositories: other_repositories)
+
+ expect { namespace.any_project_has_container_registry_tags? }.not_to exceed_query_limit(control_count + 1)
+ end
+ end
+ end
+
describe '#first_project_with_container_registry_tags' do
let(:container_repository) { create(:container_repository) }
let!(:project) { create(:project, namespace: namespace, container_repositories: [container_repository]) }
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 0777ca04deb..836ffadd7f7 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -38,6 +38,30 @@ RSpec.describe Release do
end
end
+ context 'when description of a release is longer than the limit' do
+ let(:description) { 'a' * (Gitlab::Database::MAX_TEXT_SIZE_LIMIT + 1) }
+ let(:release) { build(:release, project: project, description: description) }
+
+ it 'creates a validation error' do
+ release.validate
+
+ expect(release.errors.full_messages)
+ .to include("Description is too long (maximum is #{Gitlab::Database::MAX_TEXT_SIZE_LIMIT} characters)")
+ end
+
+ context 'when validate_release_description_length feature flag is disabled' do
+ before do
+ stub_feature_flags(validate_release_description_length: false)
+ end
+
+ it 'does not create a validation error' do
+ release.validate
+
+ expect(release.errors.full_messages).to be_empty
+ end
+ end
+ end
+
context 'when a release is tied to a milestone for another project' do
it 'creates a validation error' do
milestone = build(:milestone, project: create(:project))
diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
index 9bd71ea6f64..84c64508a8d 100644
--- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb
+++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb
@@ -5,38 +5,27 @@ require 'spec_helper'
RSpec.describe AlertManagement::ProcessPrometheusAlertService do
let_it_be(:project, reload: true) { create(:project, :repository) }
- before do
- allow(ProjectServiceWorker).to receive(:perform_async)
- end
+ let(:service) { described_class.new(project, payload) }
describe '#execute' do
- let(:service) { described_class.new(project, payload) }
- let(:source) { 'Prometheus' }
- let(:auto_close_incident) { true }
- let(:create_issue) { true }
- let(:send_email) { true }
- let(:incident_management_setting) do
- double(
- auto_close_incident?: auto_close_incident,
- create_issue?: create_issue,
- send_email?: send_email
- )
- end
+ include_context 'incident management settings enabled'
+
+ subject(:execute) { service.execute }
before do
- allow(service)
- .to receive(:incident_management_setting)
- .and_return(incident_management_setting)
+ stub_licensed_features(oncall_schedules: false, generic_alert_fingerprinting: false)
end
- subject(:execute) { service.execute }
-
context 'when alert payload is valid' do
- let(:parsed_payload) { Gitlab::AlertManagement::Payload.parse(project, payload, monitoring_tool: source) }
- let(:fingerprint) { parsed_payload.gitlab_fingerprint }
+ let_it_be(:starts_at) { '2020-04-27T10:10:22.265949279Z' }
+ let_it_be(:title) { 'Alert title' }
+ let_it_be(:fingerprint) { [starts_at, title, 'vector(1)'].join('/') }
+ let_it_be(:source) { 'Prometheus' }
+
+ let(:prometheus_status) { 'firing' }
let(:payload) do
{
- 'status' => status,
+ 'status' => prometheus_status,
'labels' => {
'alertname' => 'GitalyFileServerDown',
'channel' => 'gitaly',
@@ -46,196 +35,32 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
'annotations' => {
'description' => 'Alert description',
'runbook' => 'troubleshooting/gitaly-down.md',
- 'title' => 'Alert title'
+ 'title' => title
},
- 'startsAt' => '2020-04-27T10:10:22.265949279Z',
+ 'startsAt' => starts_at,
'endsAt' => '2020-04-27T10:20:22.265949279Z',
- 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1',
- 'fingerprint' => 'b6ac4d42057c43c1'
+ 'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1'
}
end
- let(:status) { 'firing' }
-
- context 'when Prometheus alert status is firing' do
- context 'when alert with the same fingerprint already exists' do
- let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
-
- it_behaves_like 'adds an alert management alert event'
- it_behaves_like 'processes incident issues'
- it_behaves_like 'Alert Notification Service sends notification email'
-
- context 'existing alert is resolved' do
- let!(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) }
-
- it_behaves_like 'creates an alert management alert'
- it_behaves_like 'Alert Notification Service sends notification email'
- end
-
- context 'existing alert is ignored' do
- let!(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: fingerprint) }
-
- it_behaves_like 'adds an alert management alert event'
- it_behaves_like 'Alert Notification Service sends no notifications'
- end
-
- context 'existing alert is acknowledged' do
- let!(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: fingerprint) }
-
- it_behaves_like 'adds an alert management alert event'
- it_behaves_like 'Alert Notification Service sends no notifications'
- end
-
- context 'two existing alerts, one resolved one open' do
- let!(:resolved_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint) }
- let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
-
- it_behaves_like 'adds an alert management alert event'
- it_behaves_like 'Alert Notification Service sends notification email'
- end
-
- context 'when auto-creation of issues is disabled' do
- let(:create_issue) { false }
-
- it_behaves_like 'does not process incident issues'
- end
-
- context 'when emails are disabled' do
- let(:send_email) { false }
-
- it_behaves_like 'Alert Notification Service sends no notifications'
- end
- end
-
- context 'when alert does not exist' do
- context 'when alert can be created' do
- it_behaves_like 'creates an alert management alert'
- it_behaves_like 'Alert Notification Service sends notification email'
- it_behaves_like 'processes incident issues'
-
- it_behaves_like 'creates single system note based on the source of the alert'
-
- context 'when auto-alert creation is disabled' do
- let(:create_issue) { false }
-
- it_behaves_like 'does not process incident issues'
- end
-
- context 'when emails are disabled' do
- let(:send_email) { false }
-
- it_behaves_like 'Alert Notification Service sends no notifications'
- end
- end
-
- context 'when alert cannot be created' do
- let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })}
-
- before do
- allow(service).to receive(:alert).and_call_original
- allow(service).to receive_message_chain(:alert, :save).and_return(false)
- allow(service).to receive_message_chain(:alert, :errors).and_return(errors)
- end
-
- it_behaves_like 'Alert Notification Service sends no notifications', http_status: :bad_request
- it_behaves_like 'does not process incident issues due to error', http_status: :bad_request
-
- it 'writes a warning to the log' do
- expect(Gitlab::AppLogger).to receive(:warn).with(
- message: 'Unable to create AlertManagement::Alert from Prometheus',
- project_id: project.id,
- alert_errors: { hosts: ['hosts array is over 255 chars'] }
- )
-
- execute
- end
- end
-
- it { is_expected.to be_success }
- end
- end
-
- context 'when Prometheus alert status is resolved' do
- let(:status) { 'resolved' }
- let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint, monitoring_tool: source) }
-
- context 'when auto_resolve_incident set to true' do
- context 'when status can be changed' do
- it_behaves_like 'Alert Notification Service sends notification email'
- it_behaves_like 'does not process incident issues'
-
- it 'resolves an existing alert without error' do
- expect(Gitlab::AppLogger).not_to receive(:warn)
- expect { execute }.to change { alert.reload.resolved? }.to(true)
- end
-
- it_behaves_like 'creates status-change system note for an auto-resolved alert'
-
- context 'existing issue' do
- let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: fingerprint) }
-
- it 'closes the issue' do
- issue = alert.issue
-
- expect { execute }
- .to change { issue.reload.state }
- .from('opened')
- .to('closed')
- end
-
- it 'creates a resource state event' do
- expect { execute }.to change(ResourceStateEvent, :count).by(1)
- end
- end
- end
-
- context 'when status change did not succeed' do
- before do
- allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert])
- allow(alert).to receive(:resolve).and_return(false)
- end
-
- it 'writes a warning to the log' do
- expect(Gitlab::AppLogger).to receive(:warn).with(
- message: 'Unable to update AlertManagement::Alert status to resolved',
- project_id: project.id,
- alert_id: alert.id
- )
-
- execute
- end
-
- it_behaves_like 'Alert Notification Service sends notification email'
- end
-
- it { is_expected.to be_success }
- end
+ it_behaves_like 'processes new firing alert'
- context 'when auto_resolve_incident set to false' do
- let(:auto_close_incident) { false }
+ context 'with resolving payload' do
+ let(:prometheus_status) { 'resolved' }
- it 'does not resolve an existing alert' do
- expect { execute }.not_to change { alert.reload.resolved? }
- end
-
- it_behaves_like 'creates single system note based on the source of the alert'
- end
-
- context 'when emails are disabled' do
- let(:send_email) { false }
-
- it_behaves_like 'Alert Notification Service sends no notifications'
- end
+ it_behaves_like 'processes prometheus recovery alert'
end
context 'environment given' do
let(:environment) { create(:environment, project: project) }
+ let(:alert) { project.alert_management_alerts.last }
- it 'sets the environment' do
+ before do
payload['labels']['gitlab_environment_name'] = environment.name
- execute
+ end
- alert = project.alert_management_alerts.last
+ it 'sets the environment' do
+ execute
expect(alert.environment).to eq(environment)
end
@@ -243,12 +68,14 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
context 'prometheus alert given' do
let(:prometheus_alert) { create(:prometheus_alert, project: project) }
+ let(:alert) { project.alert_management_alerts.last }
- it 'sets the prometheus alert and environment' do
+ before do
payload['labels']['gitlab_alert_id'] = prometheus_alert.prometheus_metric_id
- execute
+ end
- alert = project.alert_management_alerts.last
+ it 'sets the prometheus alert and environment' do
+ execute
expect(alert.prometheus_alert).to eq(prometheus_alert)
expect(alert.environment).to eq(prometheus_alert.environment)
@@ -259,10 +86,7 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
context 'when alert payload is invalid' do
let(:payload) { {} }
- it 'responds with bad_request' do
- expect(execute).to be_error
- expect(execute.http_status).to eq(:bad_request)
- end
+ it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request
end
end
end
diff --git a/spec/services/projects/alerting/notify_service_spec.rb b/spec/services/projects/alerting/notify_service_spec.rb
index c272ce13132..feae8f3967c 100644
--- a/spec/services/projects/alerting/notify_service_spec.rb
+++ b/spec/services/projects/alerting/notify_service_spec.rb
@@ -3,77 +3,49 @@
require 'spec_helper'
RSpec.describe Projects::Alerting::NotifyService do
- let_it_be_with_reload(:project) { create(:project, :repository) }
+ let_it_be_with_reload(:project) { create(:project) }
+
+ let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
+ let(:payload_raw) { {} }
+
+ let(:service) { described_class.new(project, payload) }
before do
- allow(ProjectServiceWorker).to receive(:perform_async)
+ stub_licensed_features(oncall_schedules: false, generic_alert_fingerprinting: false)
end
describe '#execute' do
- let(:token) { 'invalid-token' }
- let(:starts_at) { Time.current.change(usec: 0) }
- let(:fingerprint) { 'testing' }
- let(:service) { described_class.new(project, payload) }
- let_it_be(:environment) { create(:environment, project: project) }
- let(:environment) { create(:environment, project: project) }
- let(:ended_at) { nil }
- let(:payload_raw) do
- {
- title: 'alert title',
- start_time: starts_at.rfc3339,
- end_time: ended_at&.rfc3339,
- severity: 'low',
- monitoring_tool: 'GitLab RSpec',
- service: 'GitLab Test Suite',
- description: 'Very detailed description',
- hosts: ['1.1.1.1', '2.2.2.2'],
- fingerprint: fingerprint,
- gitlab_environment_name: environment.name
- }.with_indifferent_access
- end
+ include_context 'incident management settings enabled'
- let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
+ subject { service.execute(token, integration) }
- subject { service.execute(token, nil) }
+ context 'with HTTP integration' do
+ let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project) }
- shared_examples 'notifications are handled correctly' do
context 'with valid token' do
let(:token) { integration.token }
- let(:incident_management_setting) { double(send_email?: email_enabled, create_issue?: issue_enabled, auto_close_incident?: auto_close_enabled) }
- let(:email_enabled) { false }
- let(:issue_enabled) { false }
- let(:auto_close_enabled) { false }
-
- before do
- allow(service)
- .to receive(:incident_management_setting)
- .and_return(incident_management_setting)
- end
context 'with valid payload' do
- shared_examples 'assigns the alert properties' do
- it 'ensure that created alert has all data properly assigned' do
- subject
- expect(last_alert_attributes).to match(
- project_id: project.id,
- title: payload_raw.fetch(:title),
- started_at: Time.zone.parse(payload_raw.fetch(:start_time)),
- severity: payload_raw.fetch(:severity),
- status: AlertManagement::Alert.status_value(:triggered),
- events: 1,
- domain: 'operations',
- hosts: payload_raw.fetch(:hosts),
- payload: payload_raw.with_indifferent_access,
- issue_id: nil,
- description: payload_raw.fetch(:description),
- monitoring_tool: payload_raw.fetch(:monitoring_tool),
- service: payload_raw.fetch(:service),
- fingerprint: Digest::SHA1.hexdigest(fingerprint),
- environment_id: environment.id,
- ended_at: nil,
- prometheus_alert_id: nil
- )
- end
+ let_it_be(:environment) { create(:environment, project: project) }
+ let_it_be(:fingerprint) { 'testing' }
+ let_it_be(:source) { 'GitLab RSpec' }
+ let_it_be(:starts_at) { Time.current.change(usec: 0) }
+
+ let(:ended_at) { nil }
+ let(:domain) { 'operations' }
+ let(:payload_raw) do
+ {
+ title: 'alert title',
+ start_time: starts_at.rfc3339,
+ end_time: ended_at&.rfc3339,
+ severity: 'low',
+ monitoring_tool: source,
+ service: 'GitLab Test Suite',
+ description: 'Very detailed description',
+ hosts: ['1.1.1.1', '2.2.2.2'],
+ fingerprint: fingerprint,
+ gitlab_environment_name: environment.name
+ }.with_indifferent_access
end
let(:last_alert_attributes) do
@@ -82,8 +54,8 @@ RSpec.describe Projects::Alerting::NotifyService do
.with_indifferent_access
end
- it_behaves_like 'creates an alert management alert'
- it_behaves_like 'assigns the alert properties'
+ it_behaves_like 'processes new firing alert'
+ it_behaves_like 'properly assigns the alert properties'
it 'passes the integration to alert processing' do
expect(Gitlab::AlertManagement::Payload)
@@ -94,101 +66,18 @@ RSpec.describe Projects::Alerting::NotifyService do
subject
end
- it 'creates a system note corresponding to alert creation' do
- expect { subject }.to change(Note, :count).by(1)
- expect(Note.last.note).to include(payload_raw.fetch(:monitoring_tool))
- end
-
- context 'existing alert with same fingerprint' do
- let(:fingerprint_sha) { Digest::SHA1.hexdigest(fingerprint) }
- let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) }
-
- it_behaves_like 'adds an alert management alert event'
-
- context 'end time given' do
- let(:ended_at) { Time.current.change(nsec: 0) }
-
- it 'does not resolve the alert' do
- expect { subject }.not_to change { alert.reload.status }
- end
-
- it 'does not set the ended at' do
- subject
-
- expect(alert.reload.ended_at).to be_nil
- end
-
- it_behaves_like 'does not an create alert management alert'
- it_behaves_like 'creates single system note based on the source of the alert'
-
- context 'auto_close_enabled setting enabled' do
- let(:auto_close_enabled) { true }
-
- it 'resolves the alert and sets the end time', :aggregate_failures do
- subject
- alert.reload
-
- expect(alert.resolved?).to eq(true)
- expect(alert.ended_at).to eql(ended_at)
- end
-
- it_behaves_like 'creates status-change system note for an auto-resolved alert'
-
- context 'related issue exists' do
- let(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: fingerprint_sha) }
- let(:issue) { alert.issue }
-
- it { expect { subject }.to change { issue.reload.state }.from('opened').to('closed') }
- it { expect { subject }.to change(ResourceStateEvent, :count).by(1) }
- end
-
- context 'with issue enabled' do
- let(:issue_enabled) { true }
-
- it_behaves_like 'does not process incident issues'
- end
- end
- end
-
- context 'existing alert is resolved' do
- let!(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint_sha) }
-
- it_behaves_like 'creates an alert management alert'
- it_behaves_like 'assigns the alert properties'
- end
-
- context 'existing alert is ignored' do
- let!(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: fingerprint_sha) }
-
- it_behaves_like 'adds an alert management alert event'
- end
-
- context 'two existing alerts, one resolved one open' do
- let!(:resolved_existing_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: fingerprint_sha) }
- let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) }
-
- it_behaves_like 'adds an alert management alert event'
- end
- end
-
- context 'end time given' do
- let(:ended_at) { Time.current }
-
- it_behaves_like 'creates an alert management alert'
- it_behaves_like 'assigns the alert properties'
- end
-
- context 'with a minimal payload' do
- let(:payload_raw) do
+ context 'with partial payload' do
+ let_it_be(:source) { integration.name }
+ let_it_be(:payload_raw) do
{
title: 'alert title',
start_time: starts_at.rfc3339
}
end
- it_behaves_like 'creates an alert management alert'
+ include_examples 'processes never-before-seen alert'
- it 'created alert has all data properly assigned' do
+ it 'assigns the alert properties' do
subject
expect(last_alert_attributes).to match(
@@ -212,7 +101,19 @@ RSpec.describe Projects::Alerting::NotifyService do
)
end
- it_behaves_like 'creates single system note based on the source of the alert'
+ context 'with existing alert with matching payload' do
+ let_it_be(:fingerprint) { payload_raw.except(:start_time).stringify_keys }
+ let_it_be(:gitlab_fingerprint) { Gitlab::AlertManagement::Fingerprint.generate(fingerprint) }
+ let_it_be(:alert) { create(:alert_management_alert, project: project, fingerprint: gitlab_fingerprint) }
+
+ include_examples 'processes never-before-seen alert'
+ end
+ end
+
+ context 'with resolving payload' do
+ let(:ended_at) { Time.current.change(usec: 0) }
+
+ it_behaves_like 'processes recovery alert'
end
end
@@ -223,63 +124,30 @@ RSpec.describe Projects::Alerting::NotifyService do
allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object)
end
- it_behaves_like 'does not process incident issues due to error', http_status: :bad_request
- it_behaves_like 'does not an create alert management alert'
+ it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request
end
- it_behaves_like 'does not process incident issues'
-
- context 'issue enabled' do
- let(:issue_enabled) { true }
-
- it_behaves_like 'processes incident issues'
-
- context 'when alert already exists' do
- let(:fingerprint_sha) { Digest::SHA1.hexdigest(fingerprint) }
- let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) }
-
- context 'when existing alert does not have an associated issue' do
- it_behaves_like 'processes incident issues'
- end
-
- context 'when existing alert has an associated issue' do
- let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: fingerprint_sha) }
-
- it_behaves_like 'does not process incident issues'
- end
+ context 'with inactive integration' do
+ before do
+ integration.update!(active: false)
end
- end
- context 'with emails turned on' do
- let(:email_enabled) { true }
-
- it_behaves_like 'Alert Notification Service sends notification email'
+ it_behaves_like 'alerts service responds with an error and takes no actions', :forbidden
end
end
context 'with invalid token' do
- it_behaves_like 'does not process incident issues due to error', http_status: :unauthorized
- it_behaves_like 'does not an create alert management alert'
- end
- end
-
- context 'with an HTTP Integration' do
- let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project) }
+ let(:token) { 'invalid-token' }
- subject { service.execute(token, integration) }
-
- it_behaves_like 'notifications are handled correctly' do
- let(:source) { integration.name }
+ it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
end
+ end
- context 'with deactivated HTTP Integration' do
- before do
- integration.update!(active: false)
- end
+ context 'without HTTP integration' do
+ let(:integration) { nil }
+ let(:token) { nil }
- it_behaves_like 'does not process incident issues due to error', http_status: :forbidden
- it_behaves_like 'does not an create alert management alert'
- end
+ it_behaves_like 'alerts service responds with an error and takes no actions', :forbidden
end
end
end
diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
index e196220eabe..e70a23de2a0 100644
--- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb
+++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb
@@ -6,25 +6,26 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
include PrometheusHelpers
using RSpec::Parameterized::TableSyntax
- let_it_be(:project, reload: true) { create(:project) }
+ let_it_be_with_reload(:project) { create(:project) }
+ let_it_be_with_reload(:setting) do
+ create(:project_incident_management_setting, project: project, send_email: true, create_issue: true)
+ end
let(:service) { described_class.new(project, payload) }
let(:token_input) { 'token' }
- let!(:setting) do
- create(:project_incident_management_setting, project: project, send_email: true, create_issue: true)
- end
-
- let(:subject) { service.execute(token_input) }
+ subject { service.execute(token_input) }
context 'with valid payload' do
let_it_be(:alert_firing) { create(:prometheus_alert, project: project) }
let_it_be(:alert_resolved) { create(:prometheus_alert, project: project) }
- let_it_be(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
+ let_it_be(:cluster, reload: true) { create(:cluster, :provided_by_user, projects: [project]) }
+
let(:payload_raw) { prometheus_alert_payload(firing: [alert_firing], resolved: [alert_resolved]) }
let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
let(:payload_alert_firing) { payload_raw['alerts'].first }
let(:token) { 'token' }
+ let(:source) { 'Prometheus' }
context 'with environment specific clusters' do
let(:prd_cluster) do
@@ -53,11 +54,11 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
context 'without token' do
let(:token_input) { nil }
- it_behaves_like 'Alert Notification Service sends notification email'
+ include_examples 'processes one firing and one resolved prometheus alerts'
end
context 'with token' do
- it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized
+ it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
end
end
@@ -87,9 +88,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
case result = params[:result]
when :success
- it_behaves_like 'Alert Notification Service sends notification email'
+ include_examples 'processes one firing and one resolved prometheus alerts'
when :failure
- it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized
+ it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
else
raise "invalid result: #{result.inspect}"
end
@@ -97,9 +98,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
end
context 'without project specific cluster' do
- let!(:cluster) { create(:cluster, enabled: true) }
+ let_it_be(:cluster) { create(:cluster, enabled: true) }
- it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized
+ it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
end
context 'with manual prometheus installation' do
@@ -126,9 +127,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
case result = params[:result]
when :success
- it_behaves_like 'Alert Notification Service sends notification email'
+ it_behaves_like 'processes one firing and one resolved prometheus alerts'
when :failure
- it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized
+ it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
else
raise "invalid result: #{result.inspect}"
end
@@ -150,50 +151,53 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
let(:token_input) { public_send(token) if token }
let(:integration) { create(:alert_management_http_integration, active, project: project) if active }
- let(:subject) { service.execute(token_input, integration) }
+ subject { service.execute(token_input, integration) }
case result = params[:result]
when :success
- it_behaves_like 'Alert Notification Service sends notification email'
+ it_behaves_like 'processes one firing and one resolved prometheus alerts'
when :failure
- it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unauthorized
+ it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
else
raise "invalid result: #{result.inspect}"
end
end
end
- context 'alert emails' do
+ context 'incident settings' do
before do
create(:prometheus_service, project: project)
create(:project_alerting_setting, project: project, token: token)
end
- context 'when incident_management_setting does not exist' do
- let!(:setting) { nil }
-
- it 'does not send notification email', :sidekiq_might_not_need_inline do
- expect_any_instance_of(NotificationService)
- .not_to receive(:async)
+ it_behaves_like 'processes one firing and one resolved prometheus alerts'
- expect(subject).to be_success
+ context 'when incident_management_setting does not exist' do
+ before do
+ setting.destroy!
end
- end
- context 'when incident_management_setting.send_email is true' do
- it_behaves_like 'Alert Notification Service sends notification email'
+ it { is_expected.to be_success }
+ include_examples 'does not send alert notification emails'
+ include_examples 'does not process incident issues'
end
context 'incident_management_setting.send_email is false' do
- let!(:setting) do
- create(:project_incident_management_setting, send_email: false, project: project)
+ before do
+ setting.update!(send_email: false)
end
- it 'does not send notification' do
- expect(NotificationService).not_to receive(:new)
+ it { is_expected.to be_success }
+ include_examples 'does not send alert notification emails'
+ end
- expect(subject).to be_success
+ context 'incident_management_setting.create_issue is false' do
+ before do
+ setting.update!(create_issue: false)
end
+
+ it { is_expected.to be_success }
+ include_examples 'does not process incident issues'
end
end
@@ -233,7 +237,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
.and_return(false)
end
- it_behaves_like 'Alert Notification Service sends no notifications', http_status: :unprocessable_entity
+ it_behaves_like 'alerts service responds with an error and takes no actions', :unprocessable_entity
end
context 'when the payload is too big' do
@@ -244,14 +248,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do
allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object)
end
- it_behaves_like 'Alert Notification Service sends no notifications', http_status: :bad_request
-
- it 'does not process Prometheus alerts' do
- expect(AlertManagement::ProcessPrometheusAlertService)
- .not_to receive(:new)
-
- subject
- end
+ it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request
end
end
diff --git a/spec/support/shared_examples/alert_notification_service_shared_examples.rb b/spec/support/shared_examples/alert_notification_service_shared_examples.rb
deleted file mode 100644
index fc935effe0e..00000000000
--- a/spec/support/shared_examples/alert_notification_service_shared_examples.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'Alert Notification Service sends notification email' do
- let(:notification_service) { spy }
-
- it 'sends a notification' do
- expect(NotificationService)
- .to receive(:new)
- .and_return(notification_service)
-
- expect(notification_service)
- .to receive_message_chain(:async, :prometheus_alerts_fired)
-
- expect(subject).to be_success
- end
-end
-
-RSpec.shared_examples 'Alert Notification Service sends no notifications' do |http_status: nil|
- it 'does not notify' do
- expect(NotificationService).not_to receive(:new)
-
- if http_status.present?
- expect(subject).to be_error
- expect(subject.http_status).to eq(http_status)
- else
- expect(subject).to be_success
- end
- end
-end
-
-RSpec.shared_examples 'creates status-change system note for an auto-resolved alert' do
- it 'has 2 new system notes' do
- expect { subject }.to change(Note, :count).by(2)
- expect(Note.last.note).to include('Resolved')
- end
-end
-
-# Requires `source` to be defined
-RSpec.shared_examples 'creates single system note based on the source of the alert' do
- it 'has one new system note' do
- expect { subject }.to change(Note, :count).by(1)
- expect(Note.last.note).to include(source)
- end
-end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
new file mode 100644
index 00000000000..218a3462c35
--- /dev/null
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_firing_shared_examples.rb
@@ -0,0 +1,161 @@
+# frozen_string_literal: true
+
+# This shared_example requires the following variables:
+# - `service`, the service which includes AlertManagement::AlertProcessing
+RSpec.shared_examples 'creates an alert management alert or errors' do
+ it { is_expected.to be_success }
+
+ it 'creates AlertManagement::Alert' do
+ expect(Gitlab::AppLogger).not_to receive(:warn)
+
+ expect { subject }.to change(AlertManagement::Alert, :count).by(1)
+ end
+
+ it 'executes the alert service hooks' do
+ expect_next_instance_of(AlertManagement::Alert) do |alert|
+ expect(alert).to receive(:execute_services)
+ end
+
+ subject
+ end
+
+ context 'and fails to save' do
+ let(:errors) { double(messages: { hosts: ['hosts array is over 255 chars'] })}
+
+ before do
+ allow(service).to receive(:alert).and_call_original
+ allow(service).to receive_message_chain(:alert, :save).and_return(false)
+ allow(service).to receive_message_chain(:alert, :errors).and_return(errors)
+ end
+
+ it_behaves_like 'alerts service responds with an error', :bad_request
+
+ it 'writes a warning to the log' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(
+ message: "Unable to create AlertManagement::Alert from #{source}",
+ project_id: project.id,
+ alert_errors: { hosts: ['hosts array is over 255 chars'] }
+ )
+
+ subject
+ end
+ end
+end
+
+# This shared_example requires the following variables:
+# - last_alert_attributes, last created alert
+# - project, project that alert created
+# - payload_raw, hash representation of payload
+# - environment, project's environment
+# - fingerprint, fingerprint hash
+RSpec.shared_examples 'properly assigns the alert properties' do
+ specify do
+ subject
+
+ expect(last_alert_attributes).to match({
+ project_id: project.id,
+ title: payload_raw.fetch(:title),
+ started_at: Time.zone.parse(payload_raw.fetch(:start_time)),
+ severity: payload_raw.fetch(:severity, nil),
+ status: AlertManagement::Alert.status_value(:triggered),
+ events: 1,
+ domain: domain,
+ hosts: payload_raw.fetch(:hosts, nil),
+ payload: payload_raw.with_indifferent_access,
+ issue_id: nil,
+ description: payload_raw.fetch(:description, nil),
+ monitoring_tool: payload_raw.fetch(:monitoring_tool, nil),
+ service: payload_raw.fetch(:service, nil),
+ fingerprint: Digest::SHA1.hexdigest(fingerprint),
+ environment_id: environment.id,
+ ended_at: nil,
+ prometheus_alert_id: nil
+ }.with_indifferent_access)
+ end
+end
+
+RSpec.shared_examples 'does not create an alert management alert' do
+ specify do
+ expect { subject }.not_to change(AlertManagement::Alert, :count)
+ end
+end
+
+# This shared_example requires the following variables:
+# - `alert`, the alert for which events should be incremented
+RSpec.shared_examples 'adds an alert management alert event' do
+ specify do
+ expect(alert).not_to receive(:execute_services)
+
+ expect { subject }.to change { alert.reload.events }.by(1)
+
+ expect(subject).to be_success
+ end
+
+ it_behaves_like 'does not create an alert management alert'
+end
+
+# This shared_example requires the following variables:
+# - `alert`, the alert for which events should not be incremented
+RSpec.shared_examples 'does not add an alert management alert event' do
+ specify do
+ expect { subject }.not_to change { alert.reload.events }
+ end
+end
+
+RSpec.shared_examples 'processes new firing alert' do
+ include_examples 'processes never-before-seen alert'
+
+ context 'for an existing alert with the same fingerprint' do
+ let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) }
+
+ context 'which is triggered' do
+ let_it_be(:alert) { create(:alert_management_alert, :triggered, fingerprint: gitlab_fingerprint, project: project) }
+
+ it_behaves_like 'adds an alert management alert event'
+ it_behaves_like 'sends alert notification emails if enabled'
+ it_behaves_like 'processes incident issues if enabled', with_issue: true
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not create a system note for alert'
+
+ context 'with an existing resolved alert as well' do
+ let_it_be(:resolved_alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint) }
+
+ it_behaves_like 'adds an alert management alert event'
+ it_behaves_like 'sends alert notification emails if enabled'
+ it_behaves_like 'processes incident issues if enabled', with_issue: true
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not create a system note for alert'
+ end
+ end
+
+ context 'which is acknowledged' do
+ let_it_be(:alert) { create(:alert_management_alert, :acknowledged, fingerprint: gitlab_fingerprint, project: project) }
+
+ it_behaves_like 'adds an alert management alert event'
+ it_behaves_like 'processes incident issues if enabled', with_issue: true
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not create a system note for alert'
+ it_behaves_like 'does not send alert notification emails'
+ end
+
+ context 'which is ignored' do
+ let_it_be(:alert) { create(:alert_management_alert, :ignored, fingerprint: gitlab_fingerprint, project: project) }
+
+ it_behaves_like 'adds an alert management alert event'
+ it_behaves_like 'processes incident issues if enabled', with_issue: true
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not create a system note for alert'
+ it_behaves_like 'does not send alert notification emails'
+ end
+
+ context 'which is resolved' do
+ let_it_be(:alert) { create(:alert_management_alert, :resolved, fingerprint: gitlab_fingerprint, project: project) }
+
+ include_examples 'processes never-before-seen alert'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb
new file mode 100644
index 00000000000..e299a83cf97
--- /dev/null
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+# This shared_example requires the following variables:
+# - `alert`, the alert to be resolved
+RSpec.shared_examples 'resolves an existing alert management alert' do
+ it 'sets the end time and status' do
+ expect(Gitlab::AppLogger).not_to receive(:warn)
+
+ expect { subject }
+ .to change { alert.reload.resolved? }.to(true)
+ .and change { alert.ended_at.present? }.to(true)
+
+ expect(subject).to be_success
+ end
+end
+
+# This shared_example requires the following variables:
+# - `alert`, the alert not to be updated
+RSpec.shared_examples 'does not change the alert end time' do
+ specify do
+ expect { subject }.not_to change { alert.reload.ended_at }
+ end
+end
+
+# This shared_example requires the following variables:
+# - `project`, expected project for an incoming alert
+# - `service`, a service which includes AlertManagement::AlertProcessing
+# - `alert` (optional), the alert which should fail to resolve. If not
+# included, the log is expected to correspond to a new alert
+RSpec.shared_examples 'writes a warning to the log for a failed alert status update' do
+ before do
+ allow(service).to receive(:alert).and_call_original
+ allow(service).to receive_message_chain(:alert, :resolve).and_return(false)
+ end
+
+ specify do
+ expect(Gitlab::AppLogger).to receive(:warn).with(
+ message: 'Unable to update AlertManagement::Alert status to resolved',
+ project_id: project.id,
+ alert_id: alert ? alert.id : (last_alert_id + 1)
+ )
+
+ # Failure to resolve a recovery alert is not a critical failure
+ expect(subject).to be_success
+ end
+
+ private
+
+ def last_alert_id
+ AlertManagement::Alert.connection
+ .select_value("SELECT nextval('#{AlertManagement::Alert.sequence_name}')")
+ end
+end
+
+RSpec.shared_examples 'processes recovery alert' do
+ context 'seen for the first time' do
+ let(:alert) { AlertManagement::Alert.last }
+
+ include_examples 'processes never-before-seen recovery alert'
+ end
+
+ context 'for an existing alert with the same fingerprint' do
+ let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) }
+
+ context 'which is triggered' do
+ let_it_be(:alert) { create(:alert_management_alert, :triggered, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) }
+
+ it_behaves_like 'resolves an existing alert management alert'
+ it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert
+ it_behaves_like 'sends alert notification emails if enabled'
+ it_behaves_like 'closes related incident if enabled'
+ it_behaves_like 'writes a warning to the log for a failed alert status update'
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not process incident issues'
+ it_behaves_like 'does not add an alert management alert event'
+ end
+
+ context 'which is ignored' do
+ let_it_be(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) }
+
+ it_behaves_like 'resolves an existing alert management alert'
+ it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert
+ it_behaves_like 'sends alert notification emails if enabled'
+ it_behaves_like 'closes related incident if enabled'
+ it_behaves_like 'writes a warning to the log for a failed alert status update'
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not process incident issues'
+ it_behaves_like 'does not add an alert management alert event'
+ end
+
+ context 'which is acknowledged' do
+ let_it_be(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) }
+
+ it_behaves_like 'resolves an existing alert management alert'
+ it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert
+ it_behaves_like 'sends alert notification emails if enabled'
+ it_behaves_like 'closes related incident if enabled'
+ it_behaves_like 'writes a warning to the log for a failed alert status update'
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not process incident issues'
+ it_behaves_like 'does not add an alert management alert event'
+ end
+
+ context 'which is resolved' do
+ let_it_be(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) }
+
+ include_examples 'processes never-before-seen recovery alert'
+ end
+ end
+end
+
+RSpec.shared_examples 'processes prometheus recovery alert' do
+ context 'seen for the first time' do
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not send alert notification emails'
+ it_behaves_like 'does not process incident issues'
+ end
+
+ context 'for an existing alert with the same fingerprint' do
+ let_it_be(:gitlab_fingerprint) { Digest::SHA1.hexdigest(fingerprint) }
+
+ context 'which is triggered' do
+ let_it_be(:alert) { create(:alert_management_alert, :triggered, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) }
+
+ it_behaves_like 'resolves an existing alert management alert'
+ it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert
+ it_behaves_like 'sends alert notification emails if enabled'
+ it_behaves_like 'closes related incident if enabled'
+ it_behaves_like 'writes a warning to the log for a failed alert status update'
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not process incident issues'
+ it_behaves_like 'does not add an alert management alert event'
+ end
+
+ context 'which is ignored' do
+ let_it_be(:alert) { create(:alert_management_alert, :ignored, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) }
+
+ it_behaves_like 'resolves an existing alert management alert'
+ it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert
+ it_behaves_like 'sends alert notification emails if enabled'
+ it_behaves_like 'closes related incident if enabled'
+ it_behaves_like 'writes a warning to the log for a failed alert status update'
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not process incident issues'
+ it_behaves_like 'does not add an alert management alert event'
+ end
+
+ context 'which is acknowledged' do
+ let_it_be(:alert) { create(:alert_management_alert, :acknowledged, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) }
+
+ it_behaves_like 'resolves an existing alert management alert'
+ it_behaves_like 'creates expected system notes for alert', :recovery_alert, :resolve_alert
+ it_behaves_like 'sends alert notification emails if enabled'
+ it_behaves_like 'closes related incident if enabled'
+ it_behaves_like 'writes a warning to the log for a failed alert status update'
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not process incident issues'
+ it_behaves_like 'does not add an alert management alert event'
+ end
+
+ context 'which is resolved' do
+ let_it_be(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) }
+
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not send alert notification emails'
+ it_behaves_like 'does not change the alert end time'
+ it_behaves_like 'does not process incident issues'
+ it_behaves_like 'does not add an alert management alert event'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb
new file mode 100644
index 00000000000..186af49afde
--- /dev/null
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_creation_shared_examples.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+# Expects usage of 'incident settings enabled' context.
+#
+# This shared_example includes the following option:
+# - with_issue: includes a test for when the defined `alert` has an associated issue
+#
+# This shared_example requires the following variables:
+# - `alert`, required if :with_issue is true
+RSpec.shared_examples 'processes incident issues if enabled' do |with_issue: false|
+ include_examples 'processes incident issues', with_issue
+
+ context 'with incident setting disabled' do
+ let(:create_issue) { false }
+
+ it_behaves_like 'does not process incident issues'
+ end
+end
+
+RSpec.shared_examples 'processes incident issues' do |with_issue: false|
+ before do
+ allow_next_instance_of(AlertManagement::Alert) do |alert|
+ allow(alert).to receive(:execute_services)
+ end
+ end
+
+ specify do
+ expect(IncidentManagement::ProcessAlertWorker)
+ .to receive(:perform_async)
+ .with(nil, nil, kind_of(Integer))
+
+ Sidekiq::Testing.inline! do
+ expect(subject).to be_success
+ end
+ end
+
+ context 'with issue', if: with_issue do
+ before do
+ alert.update!(issue: create(:issue, project: project))
+ end
+
+ it_behaves_like 'does not process incident issues'
+ end
+end
+
+RSpec.shared_examples 'does not process incident issues' do
+ specify do
+ expect(IncidentManagement::ProcessAlertWorker).not_to receive(:perform_async)
+
+ subject
+ end
+end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb
new file mode 100644
index 00000000000..132f1e0422e
--- /dev/null
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# Expects usage of 'incident settings enabled' context.
+#
+# This shared_example requires the following variables:
+# - `alert`, alert for which related incidents should be closed
+# - `project`, project of the alert
+RSpec.shared_examples 'closes related incident if enabled' do
+ context 'with issue' do
+ before do
+ alert.update!(issue: create(:issue, project: project))
+ end
+
+ it { expect { subject }.to change { alert.issue.reload.closed? }.from(false).to(true) }
+ it { expect { subject }.to change(ResourceStateEvent, :count).by(1) }
+ end
+
+ context 'without issue' do
+ it { expect { subject }.not_to change { alert.reload.issue } }
+ it { expect { subject }.not_to change(ResourceStateEvent, :count) }
+ end
+
+ context 'with incident setting disabled' do
+ let(:auto_close_incident) { false }
+
+ it_behaves_like 'does not close related incident'
+ end
+end
+
+RSpec.shared_examples 'does not close related incident' do
+ context 'with issue' do
+ before do
+ alert.update!(issue: create(:issue, project: project))
+ end
+
+ it { expect { subject }.not_to change { alert.issue.reload.state } }
+ it { expect { subject }.not_to change(ResourceStateEvent, :count) }
+ end
+
+ context 'without issue' do
+ it { expect { subject }.not_to change { alert.reload.issue } }
+ it { expect { subject }.not_to change(ResourceStateEvent, :count) }
+ end
+end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb
new file mode 100644
index 00000000000..5f30b58176b
--- /dev/null
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/notifications_shared_examples.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# Expects usage of 'incident settings enabled' context.
+#
+# This shared_example includes the following option:
+# - count: number of notifications expected to be sent
+RSpec.shared_examples 'sends alert notification emails if enabled' do |count: 1|
+ include_examples 'sends alert notification emails', count
+
+ context 'with email setting disabled' do
+ let(:send_email) { false }
+
+ it_behaves_like 'does not send alert notification emails'
+ end
+end
+
+RSpec.shared_examples 'sends alert notification emails' do |count: 1|
+ let(:notification_async) { double(NotificationService::Async) }
+
+ specify do
+ allow(NotificationService).to receive_message_chain(:new, :async).and_return(notification_async)
+ expect(notification_async).to receive(:prometheus_alerts_fired).exactly(count).times
+
+ subject
+ end
+end
+
+RSpec.shared_examples 'does not send alert notification emails' do
+ specify do
+ expect(NotificationService).not_to receive(:new)
+
+ subject
+ end
+end
diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb
new file mode 100644
index 00000000000..57d598c0259
--- /dev/null
+++ b/spec/support/shared_examples/services/alert_management/alert_processing/system_notes_shared_examples.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+# This shared_example includes the following option:
+# - notes: any of [:new_alert, :recovery_alert, :resolve_alert].
+# Represents which notes are expected to be created.
+#
+# This shared_example requires the following variables:
+# - `source` (optional), the monitoring tool or integration name
+# expected in the applicable system notes
+RSpec.shared_examples 'creates expected system notes for alert' do |*notes|
+ let(:expected_note_count) { expected_notes.length }
+ let(:new_notes) { Note.last(expected_note_count).pluck(:note) }
+ let(:expected_notes) do
+ {
+ new_alert: source,
+ recovery_alert: source,
+ resolve_alert: 'Resolved'
+ }.slice(*notes)
+ end
+
+ it "for #{notes.join(', ')}" do
+ expect { subject }.to change(Note, :count).by(expected_note_count)
+
+ expected_notes.each_value.with_index do |value, index|
+ expect(new_notes[index]).to include(value)
+ end
+ end
+end
+
+RSpec.shared_examples 'does not create a system note for alert' do
+ specify do
+ expect { subject }.not_to change(Note, :count)
+ end
+end
diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb
index d9f28a97a0f..a0e084967e5 100644
--- a/spec/support/shared_examples/services/alert_management_shared_examples.rb
+++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb
@@ -1,111 +1,67 @@
# frozen_string_literal: true
-RSpec.shared_examples 'creates an alert management alert' do
- it { is_expected.to be_success }
+RSpec.shared_examples 'alerts service responds with an error and takes no actions' do |http_status|
+ include_examples 'alerts service responds with an error', http_status
- it 'creates AlertManagement::Alert' do
- expect { subject }.to change(AlertManagement::Alert, :count).by(1)
- end
-
- it 'executes the alert service hooks' do
- expect_next_instance_of(AlertManagement::Alert) do |alert|
- expect(alert).to receive(:execute_services)
- end
+ it_behaves_like 'does not create an alert management alert'
+ it_behaves_like 'does not create a system note for alert'
+ it_behaves_like 'does not process incident issues'
+ it_behaves_like 'does not send alert notification emails'
+end
- subject
+RSpec.shared_examples 'alerts service responds with an error' do |http_status|
+ specify do
+ expect(subject).to be_error
+ expect(subject.http_status).to eq(http_status)
end
end
# This shared_example requires the following variables:
-# - last_alert_attributes, last created alert
-# - project, project that alert created
-# - payload_raw, hash representation of payload
-# - environment, project's environment
-# - fingerprint, fingerprint hash
-RSpec.shared_examples 'assigns the alert properties' do
- it 'ensures that created alert has all data properly assigned' do
- subject
-
- expect(last_alert_attributes).to match(
- project_id: project.id,
- title: payload_raw.fetch(:title),
- started_at: Time.zone.parse(payload_raw.fetch(:start_time)),
- severity: payload_raw.fetch(:severity),
- status: AlertManagement::Alert.status_value(:triggered),
- events: 1,
- domain: domain,
- hosts: payload_raw.fetch(:hosts),
- payload: payload_raw.with_indifferent_access,
- issue_id: nil,
- description: payload_raw.fetch(:description),
- monitoring_tool: payload_raw.fetch(:monitoring_tool),
- service: payload_raw.fetch(:service),
- fingerprint: Digest::SHA1.hexdigest(fingerprint),
- environment_id: environment.id,
- ended_at: nil,
- prometheus_alert_id: nil
+# - `service`, a service which includes ::IncidentManagement::Settings
+RSpec.shared_context 'incident management settings enabled' do
+ let(:auto_close_incident) { true }
+ let(:create_issue) { true }
+ let(:send_email) { true }
+
+ let(:incident_management_setting) do
+ double(
+ auto_close_incident?: auto_close_incident,
+ create_issue?: create_issue,
+ send_email?: send_email
)
end
-end
-RSpec.shared_examples 'does not an create alert management alert' do
- it 'does not create alert' do
- expect { subject }.not_to change(AlertManagement::Alert, :count)
+ before do
+ allow(ProjectServiceWorker).to receive(:perform_async)
+ allow(service)
+ .to receive(:incident_management_setting)
+ .and_return(incident_management_setting)
end
end
-RSpec.shared_examples 'adds an alert management alert event' do
- it { is_expected.to be_success }
-
- it 'does not create an alert' do
- expect { subject }.not_to change(AlertManagement::Alert, :count)
- end
-
- it 'increases alert events count' do
- expect { subject }.to change { alert.reload.events }.by(1)
- end
-
- it 'does not executes the alert service hooks' do
- expect(alert).not_to receive(:execute_services)
-
- subject
- end
+RSpec.shared_examples 'processes never-before-seen alert' do
+ it_behaves_like 'creates an alert management alert or errors'
+ it_behaves_like 'creates expected system notes for alert', :new_alert
+ it_behaves_like 'processes incident issues if enabled'
+ it_behaves_like 'sends alert notification emails if enabled'
end
-RSpec.shared_examples 'processes incident issues' do
- let(:create_incident_service) { spy }
-
- before do
- allow_any_instance_of(AlertManagement::Alert).to receive(:execute_services)
- end
-
- it 'processes issues' do
- expect(IncidentManagement::ProcessAlertWorker)
- .to receive(:perform_async)
- .with(nil, nil, kind_of(Integer))
- .once
-
- Sidekiq::Testing.inline! do
- expect(subject).to be_success
- end
- end
+RSpec.shared_examples 'processes never-before-seen recovery alert' do
+ it_behaves_like 'creates an alert management alert or errors'
+ it_behaves_like 'creates expected system notes for alert', :new_alert
+ it_behaves_like 'sends alert notification emails if enabled'
+ it_behaves_like 'processes incident issues if enabled'
end
-RSpec.shared_examples 'does not process incident issues' do
- it 'does not process issues' do
- expect(IncidentManagement::ProcessAlertWorker)
- .not_to receive(:perform_async)
+RSpec.shared_examples 'processes one firing and one resolved prometheus alerts' do
+ it 'creates AlertManagement::Alert' do
+ expect(Gitlab::AppLogger).not_to receive(:warn)
- expect(subject).to be_success
+ expect { subject }
+ .to change(AlertManagement::Alert, :count).by(1)
+ .and change(Note, :count).by(1)
end
-end
-
-RSpec.shared_examples 'does not process incident issues due to error' do |http_status:|
- it 'does not process issues' do
- expect(IncidentManagement::ProcessAlertWorker)
- .not_to receive(:perform_async)
- expect(subject).to be_error
- expect(subject.http_status).to eq(http_status)
- end
+ it_behaves_like 'processes incident issues'
+ it_behaves_like 'sends alert notification emails', count: 1
end
diff --git a/yarn.lock b/yarn.lock
index 81bd4024065..c367c918d3c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4557,10 +4557,10 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
-dompurify@^2.2.7:
- version "2.2.7"
- resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.7.tgz#a5f055a2a471638680e779bd08fc334962d11fd8"
- integrity sha512-jdtDffdGNY+C76jvodNTu9jt5yYj59vuTUyx+wXdzcSwAGTYZDAQkQ7Iwx9zcGrA4ixC1syU4H3RZROqRxokxg==
+dompurify@^2.2.7, dompurify@^2.2.8:
+ version "2.2.8"
+ resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.8.tgz#ce88e395f6d00b6dc53f80d6b2a6fdf5446873c6"
+ integrity sha512-9H0UL59EkDLgY3dUFjLV6IEUaHm5qp3mxSqWw7Yyx4Zhk2Jn2cmLe+CNPP3xy13zl8Bqg+0NehQzkdMoVhGRww==
domutils@^1.5.1:
version "1.7.0"