Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
38e4bfea58
commit
db36dea03b
|
|
@ -1 +1 @@
|
|||
f6f340eff91d01a1e36e8c9c368d93c9bff5e4f5
|
||||
22654ba48106412ca6680366afe6a47389458720
|
||||
|
|
|
|||
2
Gemfile
2
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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 } });
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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]',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,17 @@
|
|||
/* 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_ITERATIONS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY, DEFAULT_LABEL_CURRENT];
|
||||
|
||||
export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL];
|
||||
|
||||
export const DEBOUNCE_DELAY = 200;
|
||||
|
||||
export const SortDirection = {
|
||||
descending: 'descending',
|
||||
ascending: 'ascending',
|
||||
};
|
||||
|
||||
export const DEFAULT_MILESTONES = [
|
||||
DEFAULT_LABEL_NONE,
|
||||
DEFAULT_LABEL_ANY,
|
||||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
= 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')
|
||||
|
||||
- @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
|
||||
- @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 }
|
||||
- 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 }
|
||||
|
||||
= 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 } }
|
||||
|
||||
.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' }>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix N+1 queries in namespace#any_project_has_container_registry_tags?
|
||||
merge_request: 59916
|
||||
author:
|
||||
type: performance
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Initialize conversion of ci_build_trace_chunks.build_id to bigint
|
||||
merge_request: 60346
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Cache project tag list
|
||||
merge_request: 57031
|
||||
author:
|
||||
type: performance
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Fix copy on webhook admin pages for "Issues events" and "Confidential issues
|
||||
events"
|
||||
merge_request: 60453
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
bdeb78403607d45d5eb779623d0e2aa1acf026f6aced6f1134824a35dfec7e74
|
||||
|
|
@ -0,0 +1 @@
|
|||
3cd56794ac903d9598863215a34eda62c3dc96bed78bed5b8a99fc522e319b35
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ Example response:
|
|||
```json
|
||||
[
|
||||
{
|
||||
"id": 8
|
||||
"id": 8,
|
||||
"title":null,
|
||||
"project_id":1,
|
||||
"action_name":"opened",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ explorer. GraphiQL explorer is available for:
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
1. Open the [GraphiQL explorer tool](https://gitlab.com/-/graphql-explorer).
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ Example response:
|
|||
},
|
||||
"provider_gcp": null,
|
||||
"management_project": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "cluster-3",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ Example of response
|
|||
"status": "pending"
|
||||
},
|
||||
"ref": "master",
|
||||
"artifacts": [],
|
||||
"runner": null,
|
||||
"stage": "test",
|
||||
"status": "failed",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ GET /licenses
|
|||
"Name": "Doe John"
|
||||
},
|
||||
"add_ons": {
|
||||
"GitLab_FileLocks": 1,
|
||||
"GitLab_FileLocks": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -176,7 +176,9 @@ Example responses:
|
|||
{
|
||||
"level": "watch"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"level": "custom",
|
||||
"events": {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ Example response:
|
|||
"wiki_page_events": true,
|
||||
"job_events": true,
|
||||
"comment_on_event_enabled": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 76,
|
||||
"title": "Alerts endpoint",
|
||||
|
|
|
|||
|
|
@ -1762,6 +1762,6 @@ Example response:
|
|||
"source_name": "Group three",
|
||||
"source_type": "Namespace",
|
||||
"access_level": "20"
|
||||
},
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,18 +14664,15 @@ 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|Replicated data is verified with the secondary node(s) using checksums."
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Replication Details"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Replication Details Desktop"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Replication Details Mobile"
|
||||
msgstr ""
|
||||
|
||||
msgid "Geo|Replication details"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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' }],
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(11_000)
|
||||
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 by 20%' do
|
||||
mock_efficiency(1.1)
|
||||
it 'with a time efficiency of 110%, it decreases the batch size' do
|
||||
time_efficiency = 1.1
|
||||
|
||||
expect { subject }.to change { migration.reload.batch_size }.from(10_000).to(8_000)
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]) }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
before do
|
||||
allow(service)
|
||||
.to receive(:incident_management_setting)
|
||||
.and_return(incident_management_setting)
|
||||
end
|
||||
include_context 'incident management settings enabled'
|
||||
|
||||
subject(:execute) { service.execute }
|
||||
|
||||
before do
|
||||
stub_licensed_features(oncall_schedules: false, generic_alert_fingerprinting: false)
|
||||
end
|
||||
|
||||
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,210 +35,48 @@ 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' }
|
||||
it_behaves_like 'processes new firing alert'
|
||||
|
||||
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) }
|
||||
context 'with resolving payload' do
|
||||
let(:prometheus_status) { 'resolved' }
|
||||
|
||||
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
|
||||
|
||||
context 'when auto_resolve_incident set to false' do
|
||||
let(:auto_close_incident) { false }
|
||||
|
||||
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 }
|
||||
|
||||
before do
|
||||
payload['labels']['gitlab_environment_name'] = environment.name
|
||||
end
|
||||
|
||||
it 'sets the environment' do
|
||||
payload['labels']['gitlab_environment_name'] = environment.name
|
||||
execute
|
||||
|
||||
alert = project.alert_management_alerts.last
|
||||
|
||||
expect(alert.environment).to eq(environment)
|
||||
end
|
||||
end
|
||||
|
||||
context 'prometheus alert given' do
|
||||
let(:prometheus_alert) { create(:prometheus_alert, project: project) }
|
||||
let(:alert) { project.alert_management_alerts.last }
|
||||
|
||||
before do
|
||||
payload['labels']['gitlab_alert_id'] = prometheus_alert.prometheus_metric_id
|
||||
end
|
||||
|
||||
it 'sets the prometheus alert and environment' do
|
||||
payload['labels']['gitlab_alert_id'] = prometheus_alert.prometheus_metric_id
|
||||
execute
|
||||
|
||||
alert = project.alert_management_alerts.last
|
||||
|
||||
expect(alert.prometheus_alert).to eq(prometheus_alert)
|
||||
expect(alert.environment).to eq(prometheus_alert.environment)
|
||||
end
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
let(:token) { 'invalid-token' }
|
||||
|
||||
it_behaves_like 'alerts service responds with an error and takes no actions', :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an HTTP Integration' do
|
||||
let_it_be_with_reload(:integration) { create(:alert_management_http_integration, project: project) }
|
||||
context 'without HTTP integration' do
|
||||
let(:integration) { nil }
|
||||
let(:token) { nil }
|
||||
|
||||
subject { service.execute(token, integration) }
|
||||
|
||||
it_behaves_like 'notifications are handled correctly' do
|
||||
let(:source) { integration.name }
|
||||
end
|
||||
|
||||
context 'with deactivated HTTP Integration' do
|
||||
before do
|
||||
integration.update!(active: false)
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
it_behaves_like 'processes one firing and one resolved prometheus alerts'
|
||||
|
||||
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)
|
||||
|
||||
expect(subject).to be_success
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue