Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-10 15:10:14 +00:00
parent c68905777e
commit be1b7b709e
68 changed files with 597 additions and 837 deletions

View File

@ -40,7 +40,7 @@ export function formatListIssues(listIssues) {
let listItemsCount;
const listData = listIssues.nodes.reduce((map, list) => {
listItemsCount = list.issues.count;
listItemsCount = list.issuesCount;
let sortedIssues = list.issues.edges.map((issueNode) => ({
...issueNode.node,
}));

View File

@ -1,157 +0,0 @@
<script>
import {
GlDropdown,
GlDropdownItem,
GlDropdownText,
GlSearchBoxByType,
GlDropdownDivider,
GlLoadingIcon,
} from '@gitlab/ui';
import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import { __, s__ } from '~/locale';
import projectMilestones from '../../graphql/project_milestones.query.graphql';
export default {
components: {
BoardEditableItem,
GlDropdown,
GlLoadingIcon,
GlDropdownItem,
GlDropdownText,
GlSearchBoxByType,
GlDropdownDivider,
},
data() {
return {
milestones: [],
searchTitle: '',
loading: false,
edit: false,
};
},
apollo: {
milestones: {
query: projectMilestones,
debounce: 250,
skip() {
return !this.edit;
},
variables() {
return {
fullPath: this.projectPath,
searchTitle: this.searchTitle,
state: 'active',
includeAncestors: true,
};
},
update(data) {
const edges = data?.project?.milestones?.edges ?? [];
return edges.map((item) => item.node);
},
error(error) {
this.setError({ error, message: this.$options.i18n.fetchMilestonesError });
},
},
},
computed: {
...mapGetters(['activeBoardItem']),
hasMilestone() {
return this.activeBoardItem.milestone !== null;
},
groupFullPath() {
const { referencePath = '' } = this.activeBoardItem;
return referencePath.slice(0, referencePath.indexOf('/'));
},
projectPath() {
const { referencePath = '' } = this.activeBoardItem;
return referencePath.slice(0, referencePath.indexOf('#'));
},
dropdownText() {
return this.activeBoardItem.milestone?.title ?? this.$options.i18n.noMilestone;
},
},
methods: {
...mapActions(['setActiveIssueMilestone', 'setError']),
handleOpen() {
this.edit = true;
this.$refs.dropdown.show();
},
handleClose() {
this.edit = false;
this.$refs.sidebarItem.collapse();
},
async setMilestone(milestoneId) {
this.loading = true;
this.searchTitle = '';
this.handleClose();
try {
const input = { milestoneId, projectPath: this.projectPath };
await this.setActiveIssueMilestone(input);
} catch (e) {
this.setError({ error: e, message: this.$options.i18n.updateMilestoneError });
} finally {
this.loading = false;
}
},
},
i18n: {
milestone: __('Milestone'),
noMilestone: __('No milestone'),
assignMilestone: __('Assign milestone'),
noMilestonesFound: s__('Milestones|No milestones found'),
fetchMilestonesError: __('There was a problem fetching milestones.'),
updateMilestoneError: __('An error occurred while updating the milestone.'),
},
};
</script>
<template>
<board-editable-item
ref="sidebarItem"
:title="$options.i18n.milestone"
:loading="loading"
data-testid="sidebar-milestones"
@open="handleOpen"
@close="handleClose"
>
<template v-if="hasMilestone" #collapsed>
<strong class="gl-text-gray-900">{{ activeBoardItem.milestone.title }}</strong>
</template>
<gl-dropdown
ref="dropdown"
:text="dropdownText"
:header-text="$options.i18n.assignMilestone"
block
@hide="handleClose"
>
<gl-search-box-by-type ref="search" v-model.trim="searchTitle" class="gl-m-3" />
<gl-dropdown-item
data-testid="no-milestone-item"
:is-check-item="true"
:is-checked="!activeBoardItem.milestone"
@click="setMilestone(null)"
>
{{ $options.i18n.noMilestone }}
</gl-dropdown-item>
<gl-dropdown-divider />
<gl-loading-icon v-if="$apollo.loading" class="gl-py-4" />
<template v-else-if="milestones.length > 0">
<gl-dropdown-item
v-for="milestone in milestones"
:key="milestone.id"
:is-check-item="true"
:is-checked="activeBoardItem.milestone && milestone.id === activeBoardItem.milestone.id"
data-testid="milestone-item"
@click="setMilestone(milestone.id)"
>
{{ milestone.title }}
</gl-dropdown-item>
</template>
<gl-dropdown-text v-else data-testid="no-milestones-found">
{{ $options.i18n.noMilestonesFound }}
</gl-dropdown-text>
</gl-dropdown>
</board-editable-item>
</template>

View File

@ -14,10 +14,6 @@ fragment IssueNode on Issue {
confidential
webUrl
relativePosition
milestone {
id
title
}
assignees {
nodes {
...User

View File

@ -1,12 +0,0 @@
mutation issueSetMilestone($input: UpdateIssueInput!) {
updateIssue(input: $input) {
issue {
milestone {
id
title
description
}
}
errors
}
}

View File

@ -12,11 +12,11 @@ query ListIssues(
) {
group(fullPath: $fullPath) @include(if: $isGroup) {
board(id: $boardId) {
lists(id: $id) {
lists(id: $id, issueFilters: $filters) {
nodes {
id
issuesCount
issues(first: $first, filters: $filters, after: $after) {
count
edges {
node {
...IssueNode
@ -33,11 +33,11 @@ query ListIssues(
}
project(fullPath: $fullPath) @include(if: $isProject) {
board(id: $boardId) {
lists(id: $id) {
lists(id: $id, issueFilters: $filters) {
nodes {
id
issuesCount
issues(first: $first, filters: $filters, after: $after) {
count
edges {
node {
...IssueNode

View File

@ -37,7 +37,6 @@ import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import * as types from './mutation_types';
@ -479,30 +478,6 @@ export default {
});
},
setActiveIssueMilestone: async ({ commit, getters }, input) => {
const { activeBoardItem } = getters;
const { data } = await gqlClient.mutate({
mutation: issueSetMilestoneMutation,
variables: {
input: {
iid: String(activeBoardItem.iid),
milestoneId: getIdFromGraphQLId(input.milestoneId),
projectPath: input.projectPath,
},
},
});
if (data.updateIssue.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
}
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: activeBoardItem.id,
prop: 'milestone',
value: data.updateIssue.issue.milestone,
});
},
addListItem: ({ commit }, { list, item, position, inProgress = false }) => {
commit(types.ADD_BOARD_ITEM_TO_LIST, {
listId: list.id,

View File

@ -29,8 +29,7 @@ export default {
},
triggerSource: {
type: String,
required: false,
default: 'unknown',
required: true,
},
trackExperiment: {
type: String,

View File

@ -6,6 +6,7 @@ import {
GlInfiniteScroll,
GlLoadingIcon,
GlSearchBoxByType,
GlTooltipDirective,
} from '@gitlab/ui';
import { produce } from 'immer';
import { fetchPolicies } from '~/lib/graphql';
@ -22,7 +23,7 @@ import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_b
export default {
i18n: {
dropdownHeader: s__('Switch Branch'),
dropdownHeader: s__('Switch branch'),
title: s__('Branches'),
fetchError: s__('Unable to fetch branch list for this project.'),
},
@ -35,6 +36,9 @@ export default {
GlLoadingIcon,
GlSearchBoxByType,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['projectFullPath', 'totalBranches'],
props: {
paginationLimit: {
@ -180,6 +184,8 @@ export default {
<template>
<gl-dropdown
v-if="showBranchSwitcher"
v-gl-tooltip.hover
:title="$options.i18n.dropdownHeader"
:header-text="$options.i18n.dropdownHeader"
:text="currentBranch"
icon="branch"

View File

@ -248,7 +248,7 @@ export default {
>
<template #footer>
<gl-dropdown-item v-if="directlyInviteMembers">
<sidebar-invite-members />
<sidebar-invite-members :issuable-type="issuableType" />
</gl-dropdown-item> </template
></user-select>
</template>

View File

@ -9,6 +9,17 @@ export default {
components: {
InviteMembersTrigger,
},
props: {
issuableType: {
type: String,
required: true,
},
},
computed: {
triggerSource() {
return `${this.issuableType}-assignee-dropdown`;
},
},
};
</script>
@ -18,6 +29,7 @@ export default {
:display-text="$options.displayText"
:event="$options.dataTrackEvent"
:label="$options.dataTrackLabel"
:trigger-source="triggerSource"
classes="gl-display-block gl-pl-6 gl-hover-text-decoration-none gl-hover-text-blue-800!"
/>
</template>

View File

@ -1,3 +1,5 @@
@import 'mixins_and_variables_and_functions';
.escalation-policy-modal {
width: 640px;
}
@ -9,3 +11,27 @@
.rule-close-icon {
right: 1rem;
}
$stroke-size: 1px;
.right-arrow {
@include gl-relative;
@include gl-mx-5;
@include gl-display-inline-block;
@include gl-vertical-align-middle;
height: $stroke-size;
background-color: var(--gray-900, $gray-900);
min-width: $gl-spacing-scale-7;
&-head {
@include gl-absolute;
top: -2*$stroke-size;
left: calc(100% - #{5*$stroke-size});
@include gl-display-inline-block;
@include gl-p-1;
@include gl-border-solid;
border-width: 0 $stroke-size $stroke-size 0;
border-color: var(--gray-900, $gray-900);
transform: rotate(-45deg);
}
}

View File

@ -14,7 +14,13 @@ module Ci
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
default_value_for :data_store, :redis
default_value_for :data_store do
if Feature.enabled?(:dedicated_redis_trace_chunks, type: :ops)
:redis_trace_chunks
else
:redis
end
end
after_create { metrics.increment_trace_operation(operation: :chunked) }
@ -25,22 +31,22 @@ module Ci
FailedToPersistDataError = Class.new(StandardError)
# Note: The ordering of this hash is related to the precedence of persist store.
# The bottom item takes the highest precedence, and the top item takes the lowest precedence.
DATA_STORES = {
redis: 1,
database: 2,
fog: 3
fog: 3,
redis_trace_chunks: 4
}.freeze
STORE_TYPES = DATA_STORES.keys.to_h do |store|
[store, "Ci::BuildTraceChunks::#{store.capitalize}".constantize]
[store, "Ci::BuildTraceChunks::#{store.to_s.camelize}".constantize]
end.freeze
LIVE_STORES = %i[redis redis_trace_chunks].freeze
enum data_store: DATA_STORES
scope :live, -> { redis }
scope :persisted, -> { not_redis.order(:chunk_index) }
scope :live, -> { where(data_store: LIVE_STORES) }
scope :persisted, -> { where.not(data_store: LIVE_STORES).order(:chunk_index) }
class << self
def all_stores
@ -48,8 +54,7 @@ module Ci
end
def persistable_store
# get first available store from the back of the list
all_stores.reverse.find { |store| get_store_class(store).available? }
STORE_TYPES[:fog].available? ? :fog : :database
end
def get_store_class(store)
@ -195,7 +200,7 @@ module Ci
end
def flushed?
!redis?
!live?
end
def migrated?
@ -203,7 +208,7 @@ module Ci
end
def live?
redis?
LIVE_STORES.include?(data_store.to_sym)
end
def <=>(other)

View File

@ -3,10 +3,6 @@
module Ci
module BuildTraceChunks
class Database
def available?
true
end
def keys(relation)
[]
end

View File

@ -3,10 +3,18 @@
module Ci
module BuildTraceChunks
class Fog
def available?
def self.available?
object_store.enabled
end
def self.object_store
Gitlab.config.artifacts.object_store
end
def available?
self.class.available?
end
def data(model)
files.get(key(model))&.body
rescue Excon::Error::NotFound
@ -85,7 +93,7 @@ module Ci
end
def object_store
Gitlab.config.artifacts.object_store
self.class.object_store
end
def object_store_raw_config

View File

@ -2,92 +2,11 @@
module Ci
module BuildTraceChunks
class Redis
CHUNK_REDIS_TTL = 1.week
LUA_APPEND_CHUNK = <<~EOS
local key, new_data, offset = KEYS[1], ARGV[1], ARGV[2]
local length = new_data:len()
local expire = #{CHUNK_REDIS_TTL.seconds}
local current_size = redis.call("strlen", key)
offset = tonumber(offset)
if offset == 0 then
-- overwrite everything
redis.call("set", key, new_data, "ex", expire)
return redis.call("strlen", key)
elseif offset > current_size then
-- offset range violation
return -1
elseif offset + length >= current_size then
-- efficiently append or overwrite and append
redis.call("expire", key, expire)
return redis.call("setrange", key, offset, new_data)
else
-- append and truncate
local current_data = redis.call("get", key)
new_data = current_data:sub(1, offset) .. new_data
redis.call("set", key, new_data, "ex", expire)
return redis.call("strlen", key)
end
EOS
def available?
true
end
def data(model)
Gitlab::Redis::SharedState.with do |redis|
redis.get(key(model))
end
end
def set_data(model, new_data)
Gitlab::Redis::SharedState.with do |redis|
redis.set(key(model), new_data, ex: CHUNK_REDIS_TTL)
end
end
def append_data(model, new_data, offset)
Gitlab::Redis::SharedState.with do |redis|
redis.eval(LUA_APPEND_CHUNK, keys: [key(model)], argv: [new_data, offset])
end
end
def size(model)
Gitlab::Redis::SharedState.with do |redis|
redis.strlen(key(model))
end
end
def delete_data(model)
delete_keys([[model.build_id, model.chunk_index]])
end
def keys(relation)
relation.pluck(:build_id, :chunk_index)
end
def delete_keys(keys)
return if keys.empty?
keys = keys.map { |key| key_raw(*key) }
Gitlab::Redis::SharedState.with do |redis|
# https://gitlab.com/gitlab-org/gitlab/-/issues/224171
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.del(keys)
end
end
end
class Redis < RedisBase
private
def key(model)
key_raw(model.build_id, model.chunk_index)
end
def key_raw(build_id, chunk_index)
"gitlab:ci:trace:#{build_id.to_i}:chunks:#{chunk_index.to_i}"
def with_redis
Gitlab::Redis::SharedState.with { |redis| yield(redis) }
end
end
end

View File

@ -0,0 +1,90 @@
# frozen_string_literal: true
module Ci
module BuildTraceChunks
class RedisBase
CHUNK_REDIS_TTL = 1.week
LUA_APPEND_CHUNK = <<~EOS
local key, new_data, offset = KEYS[1], ARGV[1], ARGV[2]
local length = new_data:len()
local expire = #{CHUNK_REDIS_TTL.seconds}
local current_size = redis.call("strlen", key)
offset = tonumber(offset)
if offset == 0 then
-- overwrite everything
redis.call("set", key, new_data, "ex", expire)
return redis.call("strlen", key)
elseif offset > current_size then
-- offset range violation
return -1
elseif offset + length >= current_size then
-- efficiently append or overwrite and append
redis.call("expire", key, expire)
return redis.call("setrange", key, offset, new_data)
else
-- append and truncate
local current_data = redis.call("get", key)
new_data = current_data:sub(1, offset) .. new_data
redis.call("set", key, new_data, "ex", expire)
return redis.call("strlen", key)
end
EOS
def data(model)
with_redis do |redis|
redis.get(key(model))
end
end
def set_data(model, new_data)
with_redis do |redis|
redis.set(key(model), new_data, ex: CHUNK_REDIS_TTL)
end
end
def append_data(model, new_data, offset)
with_redis do |redis|
redis.eval(LUA_APPEND_CHUNK, keys: [key(model)], argv: [new_data, offset])
end
end
def size(model)
with_redis do |redis|
redis.strlen(key(model))
end
end
def delete_data(model)
delete_keys([[model.build_id, model.chunk_index]])
end
def keys(relation)
relation.pluck(:build_id, :chunk_index)
end
def delete_keys(keys)
return if keys.empty?
keys = keys.map { |key| key_raw(*key) }
with_redis do |redis|
# https://gitlab.com/gitlab-org/gitlab/-/issues/224171
Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
redis.del(keys)
end
end
end
private
def key(model)
key_raw(model.build_id, model.chunk_index)
end
def key_raw(build_id, chunk_index)
"gitlab:ci:trace:#{build_id.to_i}:chunks:#{chunk_index.to_i}"
end
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module Ci
module BuildTraceChunks
class RedisTraceChunks < RedisBase
private
def with_redis
Gitlab::Redis::TraceChunks.with { |redis| yield(redis) }
end
end
end
end

View File

@ -16,7 +16,10 @@
.gl-w-half.gl-xs-w-full
.gl-display-flex.gl-flex-wrap.gl-justify-content-end.gl-mb-3
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full', display_text: _('Invite a group') } }
.js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
.js-invite-members-trigger{ data: { variant: 'success',
classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3',
trigger_source: 'group-members-page',
display_text: _('Invite members') } }
= render 'groups/invite_members_modal', group: @group
- if can_manage_members? && Feature.disabled?(:invite_members_group_modal, @group)
%hr.gl-mt-4

View File

@ -26,7 +26,10 @@
- if @project.allowed_to_share_with_group?
.js-invite-group-trigger{ data: { classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite a group') } }
- if can_manage_project_members?(@project) && !membership_locked?
.js-invite-members-trigger{ data: { variant: 'success', classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3', display_text: _('Invite members') } }
.js-invite-members-trigger{ data: { variant: 'success',
classes: 'gl-mt-3 gl-sm-w-auto gl-w-full gl-sm-ml-3',
trigger_source: 'project-members-page',
display_text: _('Invite members') } }
= render 'projects/invite_members_modal', project: @project
- else

View File

@ -45,4 +45,5 @@
= render 'shared/issuable/sidebar_user_dropdown',
options: options,
wrapper_class: 'js-sidebar-assignee-dropdown',
track_label: 'edit_assignee'
track_label: 'edit_assignee',
trigger_source: "#{issuable_type}-assignee-dropdown"

View File

@ -42,4 +42,5 @@
= render 'shared/issuable/sidebar_user_dropdown',
options: options,
wrapper_class: 'js-sidebar-reviewer-dropdown',
track_label: 'edit_reviewer'
track_label: 'edit_reviewer',
trigger_source: "#{issuable_type}-reviewer-dropdown"

View File

@ -15,6 +15,7 @@
.js-invite-members-trigger{ data: { trigger_element: 'anchor',
display_text: _('Invite Members'),
event: 'click_invite_members',
trigger_source: local_assigns.fetch(:trigger_source),
label: data['track-label'] } }
- else
= dropdown_tag(data['dropdown-title'], options: options)

View File

@ -0,0 +1,8 @@
---
name: dedicated_redis_trace_chunks
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62938
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1096
milestone: '14.0'
type: ops
group: team::Scalability
default_enabled: false

View File

@ -47,8 +47,8 @@ verification methods:
| Blobs | Container registry _(file system)_ | Geo with API/Docker API | _Not implemented_ |
| Blobs | Container registry _(object storage)_ | Geo with API/Managed/Docker API (*2*) | _Not implemented_ |
| Blobs | Package registry _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | Package registry _(object storage)_ | Geo with API/Managed (*2*) | SHA256 checksum |
| Blobs | Versioned Terraform State _(file system)_ | Geo with API | _Not implemented_ |
| Blobs | Package registry _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
| Blobs | Versioned Terraform State _(file system)_ | Geo with API | SHA256 checksum |
| Blobs | Versioned Terraform State _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
| Blobs | External Merge Request Diffs _(file system)_ | Geo with API | _Not implemented_ |
| Blobs | External Merge Request Diffs _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
@ -190,14 +190,14 @@ successfully, you must replicate their data using some other means.
|[Container Registry](../../packages/container_registry.md) | **Yes** (12.3) | No | No | Disabled by default. See [instructions](docker_registry.md) to enable. |
|[Content in object storage (beta)](object_storage.md) | **Yes** (12.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/13845) | No | |
|[Project designs repository](../../../user/project/issues/design_management.md) | **Yes** (12.7) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/32467) | Via Object Storage provider if supported. Native Geo support (Beta). | |
|[Package Registry for npm](../../../user/packages/npm_registry/index.md) | **Yes** (13.2) | **Yes** (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for Maven](../../../user/packages/maven_repository/index.md) | **Yes** (13.2) | **Yes** (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for Conan](../../../user/packages/conan_repository/index.md) | **Yes** (13.2) | **Yes** (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for NuGet](../../../user/packages/nuget_repository/index.md) | **Yes** (13.2) | **Yes** (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for PyPI](../../../user/packages/pypi_repository/index.md) | **Yes** (13.2) | **Yes** (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for Composer](../../../user/packages/composer_repository/index.md) | **Yes** (13.2) | **Yes** (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for generic packages](../../../user/packages/generic_packages/index.md) | **Yes** (13.5) | **Yes** (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_terraform_state_version_replication`, enabled by default. |
|[Package Registry for npm](../../../user/packages/npm_registry/index.md) | **Yes** (13.2) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for Maven](../../../user/packages/maven_repository/index.md) | **Yes** (13.2) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for Conan](../../../user/packages/conan_repository/index.md) | **Yes** (13.2) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for NuGet](../../../user/packages/nuget_repository/index.md) | **Yes** (13.2) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for PyPI](../../../user/packages/pypi_repository/index.md) | **Yes** (13.2) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for Composer](../../../user/packages/composer_repository/index.md) | **Yes** (13.2) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Package Registry for generic packages](../../../user/packages/generic_packages/index.md) | **Yes** (13.5) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.12) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_terraform_state_version_replication`, enabled by default. |
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_merge_request_diff_replication`, enabled by default. |
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [No](https://gitlab.com/groups/gitlab-org/-/epics/2810) | No | |
|[Server-side Git hooks](../../server_hooks.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | No | |
@ -205,3 +205,9 @@ successfully, you must replicate their data using some other means.
|[GitLab Pages](../../pages/index.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/589) | No | No | |
|[Dependency proxy images](../../../user/packages/dependency_proxy/index.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/259694) | No | No | Blocked on [Geo: Secondary Mimicry](https://gitlab.com/groups/gitlab-org/-/epics/1528). Note that replication of this cache is not needed for Disaster Recovery purposes because it can be recreated from external sources. |
|[Vulnerability Export](../../../user/application_security/vulnerability_report/#export-vulnerability-details) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/3111) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Not planned because they are ephemeral and sensitive. They can be regenerated on demand. |
#### Limitation of verification for files in Object Storage
GitLab managed Object Storage replication support [is in beta](object_storage.md#enabling-gitlab-managed-object-storage-replication).
Locally stored files are verified but remote stored files are not.

View File

@ -25,7 +25,20 @@ size.
You are encouraged to first read through all the steps before executing them
in your testing/production environment.
## PostgreSQL replication
## Single instance database replication
A single instance database replication is easier to set up and still provides the same Geo capabilities
as a clusterized alternative. It's useful for setups running on a single machine
or trying to evaluate Geo for a future clusterized installation.
A single instance can be expanded to a clusterized version using Patroni, which is recommended for a
highly available architecture.
Follow below the instructions on how to set up PostgreSQL replication as a single instance database.
Alternatively, you can look at the [Multi-node database replication](#multi-node-database-replication)
instructions on setting up replication with a Patroni cluster.
### PostgreSQL replication
The GitLab **primary** node where the write operations happen connects to
the **primary** database server, and **secondary** nodes
@ -48,7 +61,7 @@ WARNING:
Geo works with streaming replication. Logical replication is not supported at this time.
There is an [issue where support is being discussed](https://gitlab.com/gitlab-org/gitlab/-/issues/7420).
### Step 1. Configure the **primary** server
#### Step 1. Configure the **primary** server
1. SSH into your GitLab **primary** server and login as root:
@ -77,11 +90,7 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
This command uses your defined `external_url` in `/etc/gitlab/gitlab.rb`.
1. GitLab 10.4 and up only: Do the following to make sure the `gitlab` database user has a password defined:
NOTE:
Until FDW settings are removed in GitLab version 14.0, avoid using single or double quotes in the
password for PostgreSQL as that leads to errors when reconfiguring.
1. Define a password for the `gitlab` database user:
Generate a MD5 hash of the desired password:
@ -103,18 +112,28 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
# must be present in all application nodes.
gitlab_rails['db_password'] = '<your_password_here>'
```
1. Define a password for the database [replication user](https://wiki.postgresql.org/wiki/Streaming_Replication).
1. Omnibus GitLab already has a [replication user](https://wiki.postgresql.org/wiki/Streaming_Replication)
called `gitlab_replicator`. You must set the password for this user manually.
You are prompted to enter a password:
We will use the username defined in `/etc/gitlab/gitlab.rb` under the `postgresql['sql_replication_user']`
setting. The default value is `gitlab_replicator`, but if you changed it to something else, adapt
the instructions below.
Generate a MD5 hash of the desired password:
```shell
gitlab-ctl set-replication-password
gitlab-ctl pg-password-md5 gitlab_replicator
# Enter password: <your_password_here>
# Confirm password: <your_password_here>
# 950233c0dfc2f39c64cf30457c3b7f1e
```
This command also reads the `postgresql['sql_replication_user']` Omnibus
setting in case you have changed `gitlab_replicator` username to something
else.
Edit `/etc/gitlab/gitlab.rb`:
```ruby
# Fill with the hash generated by `gitlab-ctl pg-password-md5 gitlab_replicator`
postgresql['sql_replication_password'] = '<md5_hash_of_your_password>'
```
If you are using an external database not managed by Omnibus GitLab, you need
to create the replicator user and define a password to it manually:
@ -275,7 +294,7 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
need it when setting up the **secondary** node! The certificate is not sensitive
data.
### Step 2. Configure the **secondary** server
#### Step 2. Configure the **secondary** server
1. SSH into your GitLab **secondary** server and login as root:
@ -376,6 +395,7 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
## Database credentials password (defined previously in primary node)
## - replicate same values here as defined in primary node
##
postgresql['sql_replication_password'] = '<md5_hash_of_your_password>'
postgresql['sql_user_password'] = '<md5_hash_of_your_password>'
gitlab_rails['db_password'] = '<your_password_here>'
```
@ -395,7 +415,7 @@ There is an [issue where support is being discussed](https://gitlab.com/gitlab-o
gitlab-ctl restart postgresql
```
### Step 3. Initiate the replication process
#### Step 3. Initiate the replication process
Below we provide a script that connects the database on the **secondary** node to
the database on the **primary** node, replicates the database, and creates the
@ -461,37 +481,37 @@ data before running `pg_basebackup`.
The replication process is now complete.
## PgBouncer support (optional)
### PgBouncer support (optional)
[PgBouncer](https://www.pgbouncer.org/) may be used with GitLab Geo to pool
PostgreSQL connections. We recommend using PgBouncer if you use GitLab in a
high-availability configuration with a cluster of nodes supporting a Geo
**primary** site and two other clusters of nodes supporting a Geo **secondary** site.
One for the main database and the other for the tracking database. For more information,
PostgreSQL connections, which can improve performance even when using in a
single instance installation.
We recommend using PgBouncer if you use GitLab in a highly available
configuration with a cluster of nodes supporting a Geo **primary** site and
two other clusters of nodes supporting a Geo **secondary** site. One for the
main database and the other for the tracking database. For more information,
see [High Availability with Omnibus GitLab](../../postgresql/replication_and_failover.md).
## Patroni support
## Multi-node database replication
Support for Patroni is intended to replace `repmgr` as a
[highly available PostgreSQL solution](../../postgresql/replication_and_failover.md)
on the primary node, but it can also be used for PostgreSQL HA on a secondary
site. Similar to `repmgr`, using Patroni on a secondary node is optional.
In GitLab 14.0, Patroni replaced `repmgr` as the supported
[highly available PostgreSQL solution](../../postgresql/replication_and_failover.md).
Starting with GitLab 13.5, Patroni is available for _experimental_ use with Geo
primary and secondary sites. Due to its experimental nature, Patroni support is
subject to change without notice.
NOTE:
If you still haven't [migrated from repmgr to Patroni](#migrating-from-repmgr-to-patroni) you're highly advised to do so.
This experimental implementation has the following limitations:
### Patroni support
- Whenever `gitlab-ctl reconfigure` runs on a Patroni Leader instance, there's a
chance of the node be demoted due to the required short-time restart. To
avoid this, you can pause auto-failover by running `gitlab-ctl patroni pause`.
After a reconfigure, it resumes on its own.
Patroni is the official replication management solution for Geo. It
can be used to build a highly available cluster on the **primary** and a **secondary** Geo site.
Using Patroni on a **secondary** site is optional and you don't have to use the same amount of
nodes on each Geo site.
For instructions about how to set up Patroni on the primary site, see the
[PostgreSQL replication and failover with Omnibus GitLab](../../postgresql/replication_and_failover.md#patroni) page.
### Configuring Patroni cluster for a Geo secondary site
#### Configuring Patroni cluster for a Geo secondary site
In a Geo secondary site, the main PostgreSQL database is a read-only replica of the primary sites PostgreSQL database.
@ -503,7 +523,7 @@ configuration for the secondary site. The internal load balancer provides a sing
endpoint for connecting to the Patroni cluster's leader whenever a new leader is
elected. Be sure to use [password credentials](../../postgresql/replication_and_failover.md#database-authorization-for-patroni) and other database best practices.
#### Step 1. Configure Patroni permanent replication slot on the primary site
##### Step 1. Configure Patroni permanent replication slot on the primary site
To set up database replication with Patroni on a secondary node, we need to
configure a _permanent replication slot_ on the primary node's Patroni cluster,
@ -526,8 +546,8 @@ Leader instance**:
retry_join: %w[CONSUL_PRIMARY1_IP CONSUL_PRIMARY2_IP CONSUL_PRIMARY3_IP]
}
repmgr['enable'] = false
roles ['patroni_role']
# You need one entry for each secondary, with a unique name following PostgreSQL slot_name constraints:
#
# Configuration syntax is: 'unique_slotname' => { 'type' => 'physical' },
@ -539,15 +559,18 @@ Leader instance**:
patroni['use_pg_rewind'] = true
patroni['postgresql']['max_wal_senders'] = 8 # Use double of the amount of patroni/reserved slots (3 patronis + 1 reserved slot for a Geo secondary).
patroni['postgresql']['max_replication_slots'] = 8 # Use double of the amount of patroni/reserved slots (3 patronis + 1 reserved slot for a Geo secondary).
patroni['replication_password'] = 'PLAIN_TEXT_POSTGRESQL_REPLICATION_PASSWORD'
postgresql['md5_auth_cidr_addresses'] = [
'PATRONI_PRIMARY1_IP/32', 'PATRONI_PRIMARY2_IP/32', 'PATRONI_PRIMARY3_IP/32', 'PATRONI_PRIMARY_PGBOUNCER/32',
'PATRONI_SECONDARY1_IP/32', 'PATRONI_SECONDARY2_IP/32', 'PATRONI_SECONDARY3_IP/32', 'PATRONI_SECONDARY_PGBOUNCER/32' # We list all secondary instances as they can all become a Standby Leader
# We list all secondary instances as they can all become a Standby Leader
postgresql['md5_auth_cidr_addresses'] = %w[
PATRONI_PRIMARY1_IP/32 PATRONI_PRIMARY2_IP/32 PATRONI_PRIMARY3_IP/32 PATRONI_PRIMARY_PGBOUNCER/32
PATRONI_SECONDARY1_IP/32 PATRONI_SECONDARY2_IP/32 PATRONI_SECONDARY3_IP/32 PATRONI_SECONDARY_PGBOUNCER/32
]
postgresql['pgbouncer_user_password'] = 'PGBOUNCER_PASSWORD_HASH'
postgresql['sql_replication_password'] = 'POSTGRESQL_REPLICATION_PASSWORD_HASH'
postgresql['sql_user_password'] = 'POSTGRESQL_PASSWORD_HASH'
postgresql['listen_address'] = '0.0.0.0' # You can use a public or VPC address here instead
```
1. Reconfigure GitLab for the changes to take effect:
@ -556,7 +579,7 @@ Leader instance**:
gitlab-ctl reconfigure
```
#### Step 2. Configure the internal load balancer on the primary site
##### Step 2. Configure the internal load balancer on the primary site
To avoid reconfiguring the Standby Leader on the secondary site whenever a new
Leader is elected on the primary site, we need to set up a TCP internal load
@ -600,7 +623,7 @@ backend postgresql
Refer to your preferred Load Balancer's documentation for further guidance.
#### Step 3. Configure a PgBouncer node on the secondary site
##### Step 3. Configure a PgBouncer node on the secondary site
A production-ready and highly available configuration requires at least
three Consul nodes, a minimum of one PgBouncer node, but its recommended to have
@ -624,19 +647,23 @@ Follow the minimal configuration for the PgBouncer node:
roles ['pgbouncer_role']
# PgBouncer configuration
pgbouncer['admin_users'] = %w(pgbouncer gitlab-consul)
pgbouncer['users'] = {
'gitlab-consul': {
# Generate it with: `gitlab-ctl pg-password-md5 gitlab-consul`
password: 'GITLAB_CONSUL_PASSWORD_HASH'
},
'pgbouncer': {
# Generate it with: `gitlab-ctl pg-password-md5 pgbouncer`
password: 'PGBOUNCER_PASSWORD_HASH'
}
}
# Consul configuration
consul['watchers'] = %w(postgresql)
consul['configuration'] = {
retry_join: %w[CONSUL_SECONDARY1_IP CONSUL_SECONDARY2_IP CONSUL_SECONDARY3_IP]
}
consul['monitoring_service_discovery'] = true
```
@ -652,13 +679,13 @@ Follow the minimal configuration for the PgBouncer node:
gitlab-ctl write-pgpass --host 127.0.0.1 --database pgbouncer --user pgbouncer --hostuser gitlab-consul
```
1. Restart the PgBouncer service:
1. Reload the PgBouncer service:
```shell
gitlab-ctl restart pgbouncer
gitlab-ctl hup pgbouncer
```
#### Step 4. Configure a Standby cluster on the secondary site
##### Step 4. Configure a Standby cluster on the secondary site
NOTE:
If you are converting a secondary site to a Patroni Cluster, you must start
@ -676,21 +703,18 @@ For each Patroni instance on the secondary site:
1. Edit `/etc/gitlab/gitlab.rb` and add the following:
```ruby
roles ['consul_role', 'postgres_role']
roles ['consul_role', 'patroni_role']
consul['enable'] = true
consul['configuration'] = {
retry_join: %w[CONSUL_SECONDARY1_IP CONSUL_SECONDARY2_IP CONSUL_SECONDARY3_IP]
}
repmgr['enable'] = false
postgresql['md5_auth_cidr_addresses'] = [
'PATRONI_SECONDARY1_IP/32', 'PATRONI_SECONDARY2_IP/32', 'PATRONI_SECONDARY3_IP/32', 'PATRONI_SECONDARY_PGBOUNCER/32',
# Any other instance that needs access to the database as per documentation
]
patroni['enable'] = false
patroni['standby_cluster']['enable'] = true
patroni['standby_cluster']['host'] = 'INTERNAL_LOAD_BALANCER_PRIMARY_IP'
patroni['standby_cluster']['port'] = INTERNAL_LOAD_BALANCER_PRIMARY_PORT
@ -699,6 +723,11 @@ For each Patroni instance on the secondary site:
patroni['use_pg_rewind'] = true
patroni['postgresql']['max_wal_senders'] = 5 # A minimum of three for one replica, plus two for each additional replica
patroni['postgresql']['max_replication_slots'] = 5 # A minimum of three for one replica, plus two for each additional replica
postgresql['pgbouncer_user_password'] = 'PGBOUNCER_PASSWORD_HASH'
postgresql['sql_replication_password'] = 'POSTGRESQL_REPLICATION_PASSWORD_HASH'
postgresql['sql_user_password'] = 'POSTGRESQL_PASSWORD_HASH'
postgresql['listen_address'] = '0.0.0.0' # You can use a public or VPC address here instead
```
1. Reconfigure GitLab for the changes to take effect.
@ -708,28 +737,6 @@ For each Patroni instance on the secondary site:
gitlab-ctl reconfigure
```
1. Remove the PostgreSQL data directory:
WARNING:
If you are converting a secondary site to a Patroni Cluster, you must skip
this step on the PostgreSQL instance.
```shell
rm -rf /var/opt/gitlab/postgresql/data
```
1. Edit `/etc/gitlab/gitlab.rb` to enable Patroni:
```ruby
patroni['enable'] = true
```
1. Reconfigure GitLab for the changes to take effect:
```shell
gitlab-ctl reconfigure
```
### Migrating from repmgr to Patroni
1. Before migrating, it is recommended that there is no replication lag between the primary and secondary sites and that replication is paused. In GitLab 13.2 and later, you can pause and resume replication with `gitlab-ctl geo-replication-pause` and `gitlab-ctl geo-replication-resume` on a Geo secondary database node.

View File

@ -1158,7 +1158,7 @@ POST /projects
| `path` | string | **{check-circle}** Yes (if name isn't provided) | Repository name for new project. Generated based on name if not provided (generated as lowercase with dashes). |
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. |
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). |
| `auto_cancel_pending_pipelines` | string | **{dotted-circle}** No | Auto-cancel pending pipelines. This isn't a boolean, but enabled/disabled. |
| `auto_devops_deploy_strategy` | string | **{dotted-circle}** No | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`). |
| `auto_devops_enabled` | boolean | **{dotted-circle}** No | Enable Auto DevOps for this project. |
@ -1233,7 +1233,7 @@ POST /projects/user/:user_id
| `name` | string | **{check-circle}** Yes | The name of the new project. |
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. |
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge requests by default. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). |
| `auto_cancel_pending_pipelines` | string | **{dotted-circle}** No | Auto-cancel pending pipelines. This isn't a boolean, but enabled/disabled. |
| `auto_devops_deploy_strategy` | string | **{dotted-circle}** No | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`). |
| `auto_devops_enabled` | boolean | **{dotted-circle}** No | Enable Auto DevOps for this project. |
@ -1305,7 +1305,7 @@ PUT /projects/:id
|-------------------------------------------------------------|----------------|------------------------|-------------|
| `allow_merge_on_skipped_pipeline` | boolean | **{dotted-circle}** No | Set whether or not merge requests can be merged with skipped jobs. |
| `analytics_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private` or `enabled` |
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge request by default. |
| `approvals_before_merge` **(PREMIUM)** | integer | **{dotted-circle}** No | How many approvers should approve merge request by default. To configure approval rules, see [Merge request approvals API](merge_request_approvals.md). |
| `auto_cancel_pending_pipelines` | string | **{dotted-circle}** No | Auto-cancel pending pipelines. This isn't a boolean, but enabled/disabled. |
| `auto_devops_deploy_strategy` | string | **{dotted-circle}** No | Auto Deploy strategy (`continuous`, `manual`, or `timed_incremental`). |
| `auto_devops_enabled` | boolean | **{dotted-circle}** No | Enable Auto DevOps for this project. |

View File

@ -20,8 +20,8 @@ see which pipelines are green and which are red allowing you to
diagnose if there is a block at a particular point, or if there's
a more systemic problem you need to investigate.
You can access the dashboard from the top bar by clicking
**More > Environments**.
You can access the dashboard on the top bar by selecting
**Menu > Environments**.
![Environments Dashboard with projects](img/environments_dashboard_v12_5.png)

View File

@ -21,7 +21,7 @@ If you are migrating from another CI/CD tool, view this documentation:
- [Migrate from Jenkins](../migration/jenkins.md).
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Watch [First time GitLab & CI/CD](https://www.youtube.com/watch?v=kTNfi5z6Uvk&t=553s). This includes a quick introduction to GitLab, the first steps with CI/CD, building a Go project, running tests, using the CI/CD pipeline editor, detecting secrets and security vulnerabilities and offers more exercises for async practice.
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Watch [Intro to GitLab CI](https://www.youtube.com/watch?v=l5705U8s_nQ&t=358s). This workshop uses the Web IDE to quickly get going with building source code using CI/CD, and run unit tests.
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Watch [Intro to GitLab CI](https://www.youtube.com/watch?v=l5705U8s_nQ&t=358s). This workshop uses the Web IDE to quickly get going with building source code using CI/CD, and run unit tests.
## CI/CD process overview
@ -76,7 +76,7 @@ All of this is defined in the `.gitlab-ci.yml` file.
To create a `.gitlab-ci.yml` file:
1. Go to **Project overview > Details**.
1. On the left sidebar, select **Project information > Details**.
1. Above the file list, select the branch you want to commit to,
click the plus icon, then select **New file**:

View File

@ -1069,36 +1069,42 @@ document to ensure it links to the most recent version of the file.
## Navigation
When documenting navigation through the user interface:
- Use the exact wording as shown in the UI, including any capital letters as-is.
- Use bold text for navigation items.
When documenting navigation through the user interface, use these terms and styles.
### What to call the menus
Use these terms when referring to the main GitLab user interface
elements:
- **Top menu**: This is the top menu that spans the width of the user interface.
It includes the GitLab logo, search field, counters, and the user's avatar.
- **Top bar**: This is the top bar that spans the width of the user interface.
It includes the menu, the GitLab logo, search field, counters, and the user's avatar.
- **Left sidebar**: This is the navigation sidebar on the left of the user
interface, specific to the project or group.
- **Right sidebar**: This is the navigation sidebar on the right of the user
interface, specific to the open issue, merge request, or epic.
### How to document the left sidebar
### How to document the menus
To be consistent, use this format when you refer to the left sidebar.
To be consistent, use this format when you write about UI navigation.
- Go to your project and select **Settings > CI/CD**.
- Go to your group and select **Settings > CI/CD**.
- Go to the Admin Area (**{admin}**) and select **Overview > Projects**.
For expandable menus, use this format:
1. Go to your group and select **Settings > CI/CD**.
1. On the top bar, select **Menu > Project** and find your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **General pipelines**.
Another example:
1. On the top bar, select **Menu > Group** and find your group.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **General pipelines**.
An Admin Area example:
`1. On the top bar, select **Menu >** **{admin}** **Admin**.`
This text generates this HTML:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
## Images
Images, including screenshots, can help a reader better understand a concept.
@ -1377,10 +1383,10 @@ readability of the text.
For example, this Markdown adds little to the accompanying text:
```markdown
1. Go to **{home}** **Project overview > Details**.
1. Go to **{home}** **Project information > Details**.
```
1. Go to **{home}** **Project overview > Details**.
1. Go to **{home}** **Project information > Details**.
However, these tables might help the reader connect the text to the user
interface:

View File

@ -163,14 +163,7 @@ stageGroupDashboards.dashboard('product_planning')
.stageGroupDashboardTrailer()
```
We provide basic customization to filter out the components essential to your group's activities. By default, all components `web`, `api`, `git`, and `sidekiq` are available in the dashboard. We can change this to only show `web` and `api`, or only show `sidekiq`:
```jsonnet
stageGroupDashboards.dashboard('product_planning', components=['web', 'api']).stageGroupDashboardTrailer()
# Or
stageGroupDashboards.dashboard('product_planning', components=['sidekiq']).stageGroupDashboardTrailer()
```
We provide basic customization to filter out the components essential to your group's activities. By default, only the `web`, `api`, and `sidekiq` components are available in the dashboard, while `git` is hidden. See [how to enable available components and optional graphs](#optional-graphs).
You can also append further information or custom metrics to a dashboard. This is an example that adds some links and a total request rate on the top of the page:
@ -219,3 +212,31 @@ If you want to see the workflow in action, we've recorded a pairing session on c
available on [GitLab Unfiltered](https://youtu.be/shEd_eiUjdI).
For deeper customization and more complicated metrics, visit the [Grafonnet lib](https://github.com/grafana/grafonnet-lib) project and the [GitLab Prometheus Metrics](../administration/monitoring/prometheus/gitlab_metrics.md#gitlab-prometheus-metrics) documentation.
### Optional Graphs
Some Graphs aren't relevant for all groups, so they aren't added to
the dashboard by default. They can be added by customizing the
dashboard.
By default, only the `web`, `api`, and `sidekiq` metrics are
shown. If you wish to see the metrics from the `git` fleet (or any
other component that might be added in the future), this could be
configured as follows:
```jsonnet
stageGroupDashboards
.dashboard('source_code', components=stageGroupDashboards.supportedComponents)
.stageGroupDashboardTrailer()
```
If your group is interested in Sidekiq job durations and their
thresholds, these graphs can be added by calling the
`.addSidekiqJobDurationByUrgency` function:
```jsonnet
stageGroupDashboards
.dashboard('access')
.addSidekiqJobDurationByUrgency()
.stageGroupDashboardTrailer()
```

View File

@ -165,12 +165,12 @@ To change the password for this customers portal account:
1. Make the required changes to the **Your password** section.
1. Click **Save changes**.
## Community program subscriptions
## Community program subscriptions
### GitLab for Education
For qualifying non-profit educational institutions, the [GitLab for Education](https://about.gitlab.com/solutions/education/) program provides
the top GitLab tier, plus 50,000 CI minutes per month.
For qualifying non-profit educational institutions, the [GitLab for Education](https://about.gitlab.com/solutions/education/) program provides
the top GitLab tier, plus 50,000 CI minutes per month.
The GitLab for Education license can only be used for instructional-use or
non-commercial academic research.
@ -180,8 +180,8 @@ Find more information on how to apply and renew at
### GitLab for Open Source
For qualifying open source projects, the [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/) program provides
the top GitLab tier, plus 50,000 CI minutes per month.
For qualifying open source projects, the [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/) program provides
the top GitLab tier, plus 50,000 CI minutes per month.
You can find more information about the [program requirements](https://about.gitlab.com/solutions/open-source/join/#requirements),
[renewals](https://about.gitlab.com/solutions/open-source/join/$renewals),
@ -211,13 +211,13 @@ After you ensure that you are using OSI-approved licenses for your projects, you
###### License overview
Go to **Project Overview > Details**. Take a screenshot that includes a view of the license you've chosen for your project.
On the left sidebar, select **Project Information > Details**. Take a screenshot that includes a view of the license you've chosen for your project.
![License overview](img/license-overview.png)
###### License file
Navigate to one of the license files that you uploaded. You can usually find the license file by selecting **Project Overview > Details** and scanning the page for the license.
Navigate to one of the license files that you uploaded. You can usually find the license file by selecting **Project Information > Details** and scanning the page for the license.
Make sure the screenshot includes the title of the license.
![License file](img/license-file.png)
@ -230,7 +230,7 @@ As a result, we ask that all projects under this license are publicly visible.
Follow these instructions to take a screenshot of the publicly visible settings:
1. Go to your project and select **Settings**.
1. Go to your project and select **Settings**.
1. Expand **Visibility, project features, permissions**.
1. Set **Project Visibility** to **Public**.
1. Ensure others can request access by selecting the **Users can request access** checkbox.
@ -241,14 +241,14 @@ Follow these instructions to take a screenshot of the publicly visible settings:
NOTE:
From time to time, GitLab allows exceptions. One or two projects within a group can be private if there is a legitimate need for it, for example,
if a project holds sensitive data. Email `opensource@gitlab.com` with details of your use case to request written permission for exceptions.
if a project holds sensitive data. Email `opensource@gitlab.com` with details of your use case to request written permission for exceptions.
### GitLab for Startups
For qualifying startups, the [GitLab for Startups](https://about.gitlab.com/solutions/startups/) program provides
the top GitLab tier, plus 50,000 CI minutes per month for 12 months.
For qualifying startups, the [GitLab for Startups](https://about.gitlab.com/solutions/startups/) program provides
the top GitLab tier, plus 50,000 CI minutes per month for 12 months.
For more information, including program requirements, see the [Startup program's landing page](https://about.gitlab.com/solutions/startups/).
For more information, including program requirements, see the [Startup program's landing page](https://about.gitlab.com/solutions/startups/).
Send all questions and requests related to the GitLab for Startups program to `startups@gitlab.com`.
@ -257,7 +257,7 @@ Send all questions and requests related to the GitLab for Startups program to `s
Because these Community Programs are free of cost, regular Priority Support is not included. However, it can be purchased at a 95% discount in some cases.
If interested, email the relevant community program team: `education@gitlab.com`, `opensource@gitlab.com`, or `startups@gitlab.com`.
As a community member, you can follow this diagram to find support:
As a community member, you can follow this diagram to find support:
![Support diagram](img/support-diagram.png)

View File

@ -150,8 +150,7 @@ the following:
![Security Center Dashboard with projects](img/security_center_dashboard_v13_4.png)
To view the Security Center, from the navigation bar at the top of the page, select
**More > Security**.
To view the Security Center, on the top bar, select **Menu > Security**.
### Adding projects to the Security Center

View File

@ -12,7 +12,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
The Operations Dashboard provides a summary of each project's operational health,
including pipeline and alert status.
The dashboard can be accessed from the top bar, by clicking **More > Operations**.
To access the dashboard, on the top bar, select **Menu > Operations**.
## Adding a project to the dashboard

View File

@ -131,7 +131,7 @@ To view the published package, go to **Packages & Registries > Package Registry*
A more detailed Composer CI/CD file is also available as a `.gitlab-ci.yml` template:
1. On the left sidebar, click **Project overview**.
1. On the left sidebar, select **Project information**.
1. Above the file list, click **Set up CI/CD**. If this button is not available, select **CI/CD Configuration** and then **Edit**.
1. From the **Apply a template** list, select **Composer**.

View File

@ -34,8 +34,8 @@ of each package management system to publish different package types to the same
Let's take a look at how you might create a public place to hold all of your public packages.
1. Create a new project in GitLab. The project doesn't require any code or content. Note the project ID
that's displayed on the project overview page.
1. Create a new project in GitLab. The project doesn't require any code or content.
1. On the left sidebar, select **Project information**, and note the project ID.
1. Create an access token. All package types in the Package Registry are accessible by using
[GitLab personal access tokens](../../profile/personal_access_tokens.md).
If you're using CI/CD, you can use CI job tokens (`CI_JOB_TOKEN`) to authenticate.

View File

@ -15,7 +15,7 @@ points to. Examples for badges can be the [pipeline status](../../ci/pipelines/s
[test coverage](../../ci/pipelines/settings.md#test-coverage-report-badge), or ways to contact the
project maintainers.
![Badges on Project overview page](img/project_overview_badges_v13_10.png)
![Badges on Project information page](img/project_overview_badges_v13_10.png)
## Project badges

View File

@ -97,7 +97,7 @@ Projects include the following [features](https://about.gitlab.com/features/):
- [Security Dashboard](../application_security/security_dashboard/index.md) **(ULTIMATE)**
- [Syntax highlighting](highlighting.md): Customize
your code blocks, overriding the default language choice.
- [Badges](badges.md): Add an image to the project overview.
- [Badges](badges.md): Add an image to the **Project information** page.
- [Releases](releases/index.md): Take a snapshot of
the source, build output, metadata, and artifacts
associated with a released version of your code.

View File

@ -24,19 +24,17 @@ of the merge request.
## Enabling commit edits from upstream members
In [GitLab 13.7 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/23308),
this setting is enabled by default. It can be changed by users with Developer
permissions to the source project. Once enabled, upstream members can
retry the pipelines and jobs of the merge request:
this setting is enabled by default. It can be changed by users with the
Developer [role](../../permissions.md) for the source project. After it's enabled,
upstream members can retry the pipelines and jobs of the merge request:
1. While creating or editing a merge request, select the checkbox **Allow
commits from members who can merge to the target branch**.
1. While creating or editing a merge request, scroll to **Contribution** and
then select the **Allow commits from members who can merge to the target branch**.
checkbox.
1. Finish creating your merge request.
![Enable contribution](img/allow_collaboration.png)
1. Once the merge request is created, you can see that commits from members who
can merge to the target branch are allowed.
![Check that contribution is enabled](img/allow_collaboration_after_save.png)
After you create the merge request, the merge request widget displays a message:
**Members who can merge are allowed to add commits.**
## Pushing to the fork as the upstream member
@ -48,41 +46,39 @@ Assuming that:
- The forked project URL is `git@gitlab.com:thedude/awesome-project.git`.
- The branch of the merge request is `update-docs`.
Here's how the process would look like:
To find and work with the changes from the fork:
1. First, you need to get the changes that the merge request has introduced.
Click the **Check out branch** button that has some pre-populated
commands that you can run.
![Check out branch button](img/checkout_button.png)
1. Use the copy button to copy the first command and paste them
in your terminal:
1. Open the merge request page, and select the **Overview** tab.
1. Scroll to the merge request widget, and select **Check out branch**:
![Check out branch button](img/commit-button_v13_12.png)
1. In the modal window, select **{copy-to-clipboard}** (**Copy**) for step 1
to copy the `git fetch` and `git checkout` instructions to your clipboard.
Paste the commands (which look like this example) into your terminal:
```shell
git fetch git@gitlab.com:thedude/awesome-project.git update-docs
git checkout -b thedude-awesome-project-update-docs FETCH_HEAD
```
This fetches the branch of the forked project and then create a local branch
These commands fetch the branch from the forked project, and create a local branch
based off the fetched branch.
1. Make any changes you want and commit.
1. Push to the forked project:
1. Make your changes to the local copy of the branch, and then commit them.
1. In your terminal, push your local changes back up to the forked project. This
command pushes the local branch `thedude-awesome-project-update-docs` to the
`update-docs` branch of the `git@gitlab.com:thedude/awesome-project.git` repository:
```shell
git push git@gitlab.com:thedude/awesome-project.git thedude-awesome-project-update-docs:update-docs
```
Note the colon (`:`) between the two branches. The above command pushes the
local branch `thedude-awesome-project-update-docs` to the
`update-docs` branch of the `git@gitlab.com:thedude/awesome-project.git` repository.
Note the colon (`:`) between the two branches.
## Troubleshooting
### Pipeline status unavailable from MR page of forked project
When a user forks a project, the permissions on the forked copy are not copied over
When a user forks a project, the permissions of the forked copy are not copied
from the original project. The creator of the fork must grant permissions to the
forked copy before members in the upstream project can view or merge the changes
in the merge request.

View File

@ -42,7 +42,7 @@ for more control of the level of oversight and security your project needs, incl
- [Require security team approval.](settings.md#security-approvals-in-merge-requests)
You can configure your merge request approval rules and settings through the GitLab
user interface or [with the API](../../../../api/merge_request_approvals.md).
user interface or with the [Merge request approvals API](../../../../api/merge_request_approvals.md).
## Approve a merge request

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -38,8 +38,8 @@ You can assign **group milestones** to any issue or merge request of any project
To view the group milestone list, in a group, go to **{issues}** **Issues > Milestones**.
You can also view all milestones you have access to in the dashboard milestones list.
To view both project milestones and group milestones you have access to, select **More > Milestones**
on the top navigation bar.
To view both project milestones and group milestones you have access to, select **Menu > Milestones**
on the top bar.
For information about project and group milestones API, see:

View File

@ -17,7 +17,7 @@ Your GitLab repository should contain files specific to an SSG, or plain HTML.
After you complete these steps, you may need to do additional
configuration for the Pages site to generate properly.
1. In the left sidebar, click **Project overview**.
1. On the left sidebar, select **Project information**.
1. Click **Set up CI/CD**.
![setup GitLab CI/CD](../img/setup_ci_v13_1.png)

View File

@ -33,7 +33,7 @@ and attach [release assets](#release-assets), like runbooks or packages.
To view a list of releases:
- Go to **Project overview > Releases**, or
- On the left sidebar, select **Project information > Releases**, or
- On the project's overview page, if at least one release exists, click the number of releases.
@ -64,8 +64,7 @@ Read more about [Release permissions](../../../user/permissions.md#project-membe
To create a new release through the GitLab UI:
1. Navigate to **Project overview > Releases** and click the **New release**
button.
1. On the left sidebar, select **Project information > Releases** and select **New release**.
1. Open the [**Tag name**](#tag-name) dropdown. Select an existing tag or type
in a new tag name. Selecting an existing tag that is already associated with
a release will result in a validation error.
@ -105,7 +104,7 @@ Read more about [Release permissions](../../../user/permissions.md#project-membe
To edit the details of a release:
1. Navigate to **Project overview > Releases**.
1. On the left sidebar, select **Project information > Releases**.
1. In the top-right corner of the release you want to modify, click **Edit this release** (the pencil icon).
1. On the **Edit Release** page, change the release's details.
1. Click **Save changes**.
@ -151,12 +150,12 @@ the [Releases API](../../../api/releases/index.md#create-a-release).
In the user interface, to associate milestones to a release:
1. Navigate to **Project overview > Releases**.
1. On the left sidebar, select **Project information > Releases**.
1. In the top-right corner of the release you want to modify, click **Edit this release** (the pencil icon).
1. From the **Milestones** list, select each milestone you want to associate. You can select multiple milestones.
1. Click **Save changes**.
On the **Project overview > Releases** page, the **Milestone** is listed in the top
On the **Project information > Releases** page, the **Milestone** is listed in the top
section, along with statistics about the issues in the milestones.
![A Release with one associated milestone](img/release_with_milestone_v12_9.png)
@ -176,7 +175,7 @@ You can be notified by email when a new release is created for your project.
To subscribe to notifications for releases:
1. Navigate to **Project overview**.
1. On the left sidebar, select **Project information**.
1. Click **Notification setting** (the bell icon).
1. In the list, click **Custom**.
1. Select the **New release** check box.
@ -210,7 +209,7 @@ deploy_to_production:
To set a deploy freeze window in the UI, complete these steps:
1. Sign in to GitLab as a user with the [Maintainer role](../../permissions.md).
1. Navigate to **Project overview**.
1. On the left sidebar, select **Project information**.
1. In the left navigation menu, navigate to **Settings > CI/CD**.
1. Scroll to **Deploy freezes**.
1. Click **Expand** to see the deploy freeze table.

View File

@ -113,7 +113,7 @@ You can download the source code that's stored in a repository.
## Repository languages
For the default branch of each repository, GitLab determines which programming languages
are used. This information is displayed on the project overview page.
are used. This information is displayed on the **Project information** page.
![Repository Languages bar](img/repository_languages_v12_2.gif)
@ -121,7 +121,7 @@ When new files are added, this information can take up to five minutes to update
### Add repository languages
Not all files are detected and listed on the project overview page. Documentation,
Not all files are detected and listed on the **Project information** page. Documentation,
vendor code, and most markup languages are excluded.
You can change this behavior by overriding the default settings.
@ -200,7 +200,7 @@ To render an OpenAPI file:
## Repository size
The project overview page shows the size of all files in the repository. The size is
The **Project information** page shows the size of all files in the repository. The size is
updated, at most, every 15 minutes. The file size includes repository files, artifacts, and LFS.
The size can differ slightly from one instance to another due to compression, housekeeping, and other factors.

View File

@ -228,10 +228,16 @@ Read through the documentation on [project settings](settings/index.md).
## Project activity
To view the activity of a project, navigate to **Project overview > Activity**.
From there, you can click on the tabs to see **All** the activity, or see it
filtered by **Push events**, **Merge events**, **Issue events**, **Comments**,
**Team**, and **Wiki**.
To view the activity of a project:
1. On the left sidebar, select **Project information > Activity**.
1. Select a tab to view **All** the activity, or to filter it by any of these criteria:
- **Push events**
- **Merge events**
- **Issue events**
- **Comments**
- **Team**
- **Wiki**
### Leave a project

View File

@ -57,11 +57,11 @@ In GitLab versions 13.0 and later, snippets are [versioned by default](#versione
To discover all snippets visible to you in GitLab, you can:
- **View all snippets visible to you**: In the top navigation bar of your GitLab
instance, go to **More > Snippets** to view your snippets dashboard.
- **View all snippets visible to you**: On the top bar of your GitLab
instance, select **Menu > Snippets** to view your snippets dashboard.
- **Visit [GitLab snippets](https://gitlab.com/dashboard/snippets)** for your snippets on GitLab.com.
- **Explore all public snippets**: In the top navigation bar of your GitLab
instance, go to **More > Snippets** and select **Explore snippets** to view
- **Explore all public snippets**: On the top bar of your GitLab
instance, select **Menu > Snippets** and select **Explore snippets** to view
[all public snippets](https://gitlab.com/explore/snippets).
- **View a project's snippets**: In your project,
go to **Snippets**.

View File

@ -23,7 +23,7 @@ module API
requires :email, types: [String, Array[String]], email_or_email_list: true, desc: 'The email address to invite, or multiple emails separated by comma'
requires :access_level, type: Integer, values: Gitlab::Access.all_values, desc: 'A valid access level (defaults: `30`, developer access level)'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'api'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'invitations-api'
end
post ":id/invitations" do
params[:source] = find_source(source_type, params[:id])

View File

@ -93,7 +93,7 @@ module API
requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
requires :user_id, types: [Integer, String], desc: 'The user ID of the new member or multiple IDs separated by commas.'
optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'api'
optional :invite_source, type: String, desc: 'Source that triggered the member creation process', default: 'members-api'
end
# rubocop: disable CodeReuse/ActiveRecord
post ":id/members" do

View File

@ -40,24 +40,24 @@ module Gitlab
TRANSLATION_LEVELS = {
'bg' => 1,
'cs_CZ' => 1,
'de' => 19,
'de' => 18,
'en' => 100,
'eo' => 1,
'es' => 41,
'es' => 40,
'fil_PH' => 1,
'fr' => 14,
'fr' => 13,
'gl_ES' => 1,
'id_ID' => 0,
'it' => 2,
'ja' => 45,
'ko' => 14,
'ja' => 44,
'ko' => 13,
'nl_NL' => 1,
'pl_PL' => 1,
'pt_BR' => 22,
'ru' => 32,
'pl_PL' => 3,
'pt_BR' => 21,
'ru' => 30,
'tr_TR' => 17,
'uk' => 43,
'zh_CN' => 72,
'uk' => 42,
'zh_CN' => 69,
'zh_HK' => 3,
'zh_TW' => 4
}.freeze

View File

@ -3750,9 +3750,6 @@ msgstr ""
msgid "An error occurred while updating the configuration."
msgstr ""
msgid "An error occurred while updating the milestone."
msgstr ""
msgid "An error occurred while updating the notification settings. Please try again."
msgstr ""
@ -13063,9 +13060,15 @@ msgstr ""
msgid "EscalationPolicies|Add escalation policy"
msgstr ""
msgid "EscalationPolicies|Add policy"
msgstr ""
msgid "EscalationPolicies|Create an escalation policy in GitLab"
msgstr ""
msgid "EscalationPolicies|Delete escalation policy"
msgstr ""
msgid "EscalationPolicies|Edit escalation policy"
msgstr ""
@ -13075,12 +13078,18 @@ msgstr ""
msgid "EscalationPolicies|Email on-call user in schedule"
msgstr ""
msgid "EscalationPolicies|Escalation policies"
msgstr ""
msgid "EscalationPolicies|Escalation rules"
msgstr ""
msgid "EscalationPolicies|Failed to load oncall-schedules"
msgstr ""
msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} %{then} THEN %{doAction} %{schedule}"
msgstr ""
msgid "EscalationPolicies|IF alert is not %{alertStatus} in %{minutes} minutes"
msgstr ""
@ -13096,6 +13105,9 @@ msgstr ""
msgid "EscalationPolicies|THEN %{doAction} %{schedule}"
msgstr ""
msgid "EscalationPolicies|mins"
msgstr ""
msgid "Estimate"
msgstr ""
@ -21249,9 +21261,6 @@ msgstr ""
msgid "Milestones|Milestone %{milestoneTitle} was not found"
msgstr ""
msgid "Milestones|No milestones found"
msgstr ""
msgid "Milestones|Ongoing Issues (open and assigned)"
msgstr ""
@ -31641,7 +31650,7 @@ msgstr ""
msgid "Survey Response"
msgstr ""
msgid "Switch Branch"
msgid "Switch branch"
msgstr ""
msgid "Switch branch/tag"

View File

@ -308,6 +308,21 @@ RSpec.describe 'Admin::Users' do
end
end
end
context 'user group count', :js do
before do
group = create(:group)
group.add_developer(current_user)
project = create(:project, group: create(:group))
project.add_reporter(current_user)
end
it 'displays count of the users authorized groups' do
wait_for_requests
expect(page.find("[data-testid='user-group-count-#{current_user.id}']").text).to eq("2")
end
end
end
describe 'GET /admin/users/new' do
@ -557,32 +572,6 @@ RSpec.describe 'Admin::Users' do
end
end
# TODO: Move to main GET /admin/users block once feature flag is removed. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/290737
context 'with vue_admin_users feature flag enabled', :js do
before do
stub_feature_flags(vue_admin_users: true)
end
describe 'GET /admin/users' do
context 'user group count', :js do
before do
group = create(:group)
group.add_developer(current_user)
project = create(:project, group: create(:group))
project.add_reporter(current_user)
end
it 'displays count of the users authorized groups' do
visit admin_users_path
wait_for_requests
expect(page.find("[data-testid='user-group-count-#{current_user.id}']").text).to eq("2")
end
end
end
end
def click_user_dropdown_toggle(user_id)
page.within("[data-testid='user-actions-#{user_id}']") do
find("[data-testid='dropdown-toggle']").click

View File

@ -80,7 +80,7 @@ RSpec.describe 'Groups > Members > Manage members' do
expect_snowplow_event(
category: 'Members::CreateService',
action: 'create_member',
label: 'unknown',
label: 'group-members-page',
property: 'existing_user',
user: user1
)
@ -189,7 +189,7 @@ RSpec.describe 'Groups > Members > Manage members' do
expect_snowplow_event(
category: 'Members::InviteService',
action: 'create_member',
label: 'unknown',
label: 'group-members-page',
property: 'net_new_user',
user: user1
)

View File

@ -59,7 +59,7 @@ RSpec.describe 'Project members list', :js do
expect_snowplow_event(
category: 'Members::CreateService',
action: 'create_member',
label: 'unknown',
label: 'project-members-page',
property: 'existing_user',
user: user1
)
@ -117,7 +117,7 @@ RSpec.describe 'Project members list', :js do
expect_snowplow_event(
category: 'Members::InviteService',
action: 'create_member',
label: 'unknown',
label: 'project-members-page',
property: 'net_new_user',
user: user1
)

View File

@ -1,176 +0,0 @@
import { GlLoadingIcon, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { mockMilestone as TEST_MILESTONE } from 'jest/boards/mock_data';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import { createStore } from '~/boards/stores';
const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, referencePath: 'h/b#2' };
describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () => {
let wrapper;
let store;
afterEach(() => {
wrapper.destroy();
store = null;
wrapper = null;
});
const createWrapper = ({ milestone = null, loading = false } = {}) => {
store = createStore();
store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } };
store.state.activeId = TEST_ISSUE.id;
wrapper = shallowMount(BoardSidebarMilestoneSelect, {
store,
provide: {
canUpdate: true,
},
data: () => ({
milestones: [TEST_MILESTONE],
}),
stubs: {
'board-editable-item': BoardEditableItem,
},
mocks: {
$apollo: {
loading,
},
},
});
};
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
const findLoader = () => wrapper.find(GlLoadingIcon);
const findDropdown = () => wrapper.find(GlDropdown);
const findBoardEditableItem = () => wrapper.find(BoardEditableItem);
const findDropdownItem = () => wrapper.find('[data-testid="milestone-item"]');
const findUnsetMilestoneItem = () => wrapper.find('[data-testid="no-milestone-item"]');
const findNoMilestonesFoundItem = () => wrapper.find('[data-testid="no-milestones-found"]');
describe('when not editing', () => {
it('opens the milestone dropdown on clicking edit', async () => {
createWrapper();
wrapper.vm.$refs.dropdown.show = jest.fn();
await findBoardEditableItem().vm.$emit('open');
expect(wrapper.vm.$refs.dropdown.show).toHaveBeenCalledTimes(1);
});
});
describe('when editing', () => {
beforeEach(() => {
createWrapper();
jest.spyOn(wrapper.vm.$refs.sidebarItem, 'collapse');
});
it('collapses BoardEditableItem on clicking edit', async () => {
await findBoardEditableItem().vm.$emit('close');
expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
});
it('collapses BoardEditableItem on hiding dropdown', async () => {
await findDropdown().vm.$emit('hide');
expect(wrapper.vm.$refs.sidebarItem.collapse).toHaveBeenCalledTimes(1);
});
});
it('renders "None" when no milestone is selected', () => {
createWrapper();
expect(findCollapsed().text()).toBe('None');
});
it('renders milestone title when set', () => {
createWrapper({ milestone: TEST_MILESTONE });
expect(findCollapsed().text()).toContain(TEST_MILESTONE.title);
});
it('shows loader while Apollo is loading', async () => {
createWrapper({ milestone: TEST_MILESTONE, loading: true });
expect(findLoader().exists()).toBe(true);
});
it('shows message when error or no milestones found', async () => {
createWrapper();
await wrapper.setData({ milestones: [] });
expect(findNoMilestonesFoundItem().text()).toBe('No milestones found');
});
describe('when milestone is selected', () => {
beforeEach(async () => {
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE.id].milestone = TEST_MILESTONE;
});
findDropdownItem().vm.$emit('click');
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders selected milestone', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toContain(TEST_MILESTONE.title);
});
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({
milestoneId: TEST_MILESTONE.id,
projectPath: 'h/b',
});
});
});
describe('when milestone is set to "None"', () => {
beforeEach(async () => {
createWrapper({ milestone: TEST_MILESTONE });
jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE.id].milestone = null;
});
findUnsetMilestoneItem().vm.$emit('click');
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders "None"', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toBe('None');
});
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueMilestone).toHaveBeenCalledWith({
milestoneId: null,
projectPath: 'h/b',
});
});
});
describe('when the mutation fails', () => {
const testMilestone = { id: '1', title: 'Former milestone' };
beforeEach(async () => {
createWrapper({ milestone: testMilestone });
jest.spyOn(wrapper.vm, 'setActiveIssueMilestone').mockImplementation(() => {
throw new Error(['failed mutation']);
});
jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
findDropdownItem().vm.$emit('click');
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders former milestone', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findCollapsed().text()).toContain(testMilestone.title);
expect(wrapper.vm.setError).toHaveBeenCalled();
});
});
});

View File

@ -30,7 +30,6 @@ import {
mockIssue2,
rawIssue,
mockIssues,
mockMilestone,
labels,
mockActiveIssue,
mockGroupProjects,
@ -1495,60 +1494,6 @@ describe('setActiveItemSubscribed', () => {
});
});
describe('setActiveIssueMilestone', () => {
const state = { boardItems: { [mockIssue.id]: mockIssue } };
const getters = { activeBoardItem: mockIssue };
const testMilestone = {
...mockMilestone,
id: 'gid://gitlab/Milestone/1',
};
const input = {
milestoneId: testMilestone.id,
projectPath: 'h/b',
};
it('should commit milestone after setting the issue', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
updateIssue: {
issue: {
milestone: testMilestone,
},
errors: [],
},
},
});
const payload = {
itemId: getters.activeBoardItem.id,
prop: 'milestone',
value: testMilestone,
};
testAction(
actions.setActiveIssueMilestone,
input,
{ ...state, ...getters },
[
{
type: types.UPDATE_BOARD_ITEM_BY_ID,
payload,
},
],
[],
done,
);
});
it('throws error if fails', async () => {
jest
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
await expect(actions.setActiveIssueMilestone({ getters }, input)).rejects.toThrow(Error);
});
});
describe('setActiveItemTitle', () => {
const state = {
boardItems: { [mockIssue.id]: mockIssue },

View File

@ -7,6 +7,8 @@ import eventHub from '~/invite_members/event_hub';
jest.mock('~/experimentation/experiment_tracking');
const displayText = 'Invite team members';
const triggerSource = '_trigger_source_';
let wrapper;
let triggerProps;
let findButton;
@ -26,7 +28,7 @@ const createComponent = (props = {}) => {
};
describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement) => {
triggerProps = { triggerElement };
triggerProps = { triggerElement, triggerSource };
findButton = () => wrapper.findComponent(triggerComponent[triggerElement]);
afterEach(() => {
@ -48,22 +50,14 @@ describe.each(['button', 'anchor'])('with triggerElement as %s', (triggerElement
spy = jest.spyOn(eventHub, '$emit');
});
it('emits openModal from an unknown source', () => {
createComponent();
findButton().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('openModal', { inviteeType: 'members', source: 'unknown' });
});
it('emits openModal from a named source', () => {
createComponent({ triggerSource: '_trigger_source_' });
createComponent();
findButton().vm.$emit('click');
expect(spy).toHaveBeenCalledWith('openModal', {
inviteeType: 'members',
source: '_trigger_source_',
source: triggerSource,
});
});
});

View File

@ -4,11 +4,16 @@ import SidebarInviteMembers from '~/sidebar/components/assignees/sidebar_invite_
describe('Sidebar invite members component', () => {
let wrapper;
const issuableType = 'issue';
const findDirectInviteLink = () => wrapper.findComponent(InviteMembersTrigger);
const createComponent = () => {
wrapper = shallowMount(SidebarInviteMembers);
wrapper = shallowMount(SidebarInviteMembers, {
propsData: {
issuableType,
},
});
};
afterEach(() => {
@ -23,5 +28,9 @@ describe('Sidebar invite members component', () => {
it('renders a direct link to project members path', () => {
expect(findDirectInviteLink().exists()).toBe(true);
});
it('has expected attributes on the trigger', () => {
expect(findDirectInviteLink().props('triggerSource')).toBe('issue-assignee-dropdown');
});
});
});

View File

@ -17,7 +17,7 @@ RSpec.describe 'Mailer retries' do
descendants.each { |a| a.queue_adapter = :test }
end
it 'sets retries for mailers to 3' do
it 'sets retries for mailers to 3', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332645' do
DeviseMailer.user_admin_approval(create(:user)).deliver_later
expect(Sidekiq::Queues['mailers'].first).to include('retry' => 3)

View File

@ -2,13 +2,13 @@
require 'spec_helper'
RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_gitlab_redis_trace_chunks do
include ExclusiveLeaseHelpers
let_it_be(:build) { create(:ci_build, :running) }
let(:chunk_index) { 0 }
let(:data_store) { :redis }
let(:data_store) { :redis_trace_chunks }
let(:raw_data) { nil }
let(:build_trace_chunk) do
@ -22,6 +22,13 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
stub_artifacts_object_storage
end
def redis_instance
{
redis: Gitlab::Redis::SharedState,
redis_trace_chunks: Gitlab::Redis::TraceChunks
}[data_store]
end
describe 'chunk creation' do
let(:metrics) { spy('metrics') }
@ -85,7 +92,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
def external_data_counter
Gitlab::Redis::SharedState.with do |redis|
redis_instance.with do |redis|
redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size
end
end
@ -101,24 +108,16 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
subject { described_class.all_stores }
it 'returns a correctly ordered array' do
is_expected.to eq(%i[redis database fog])
end
it 'returns redis store as the lowest precedence' do
expect(subject.first).to eq(:redis)
end
it 'returns fog store as the highest precedence' do
expect(subject.last).to eq(:fog)
is_expected.to eq(%i[redis database fog redis_trace_chunks])
end
end
describe '#data' do
subject { build_trace_chunk.data }
context 'when data_store is redis' do
let(:data_store) { :redis }
where(:data_store) { %i[redis redis_trace_chunks] }
with_them do
before do
build_trace_chunk.send(:unsafe_set_data!, +'Sample data in redis')
end
@ -148,6 +147,22 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
describe '#data_store' do
subject { described_class.new.data_store }
context 'default value' do
it { expect(subject).to eq('redis_trace_chunks') }
context 'when dedicated_redis_trace_chunks is disabled' do
before do
stub_feature_flags(dedicated_redis_trace_chunks: false)
end
it { expect(subject).to eq('redis') }
end
end
end
describe '#get_store_class' do
using RSpec::Parameterized::TableSyntax
@ -155,6 +170,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
:redis | Ci::BuildTraceChunks::Redis
:database | Ci::BuildTraceChunks::Database
:fog | Ci::BuildTraceChunks::Fog
:redis_trace_chunks | Ci::BuildTraceChunks::RedisTraceChunks
end
with_them do
@ -302,9 +318,9 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
context 'when data_store is redis' do
let(:data_store) { :redis }
where(:data_store) { %i[redis redis_trace_chunks] }
with_them do
context 'when there are no data' do
let(:data) { +'' }
@ -441,8 +457,9 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
context 'when data_store is redis' do
let(:data_store) { :redis }
where(:data_store) { %i[redis redis_trace_chunks] }
with_them do
let(:data) { +'Sample data in redis' }
before do
@ -475,9 +492,9 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
describe '#size' do
subject { build_trace_chunk.size }
context 'when data_store is redis' do
let(:data_store) { :redis }
where(:data_store) { %i[redis redis_trace_chunks] }
with_them do
context 'when data exists' do
let(:data) { +'Sample data in redis' }
@ -537,9 +554,14 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
subject { build_trace_chunk.persist_data! }
context 'when data_store is redis' do
let(:data_store) { :redis }
where(:data_store, :redis_class) do
[
[:redis, Ci::BuildTraceChunks::Redis],
[:redis_trace_chunks, Ci::BuildTraceChunks::RedisTraceChunks]
]
end
with_them do
context 'when data exists' do
before do
build_trace_chunk.send(:unsafe_set_data!, data)
@ -549,15 +571,15 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
let(:data) { +'a' * described_class::CHUNK_SIZE }
it 'persists the data' do
expect(build_trace_chunk.redis?).to be_truthy
expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
expect(build_trace_chunk.data_store).to eq(data_store.to_s)
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
subject
expect(build_trace_chunk.fog?).to be_truthy
expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to be_nil
expect(redis_class.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to eq(data)
end
@ -575,8 +597,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
it 'does not persist the data and the orignal data is intact' do
expect { subject }.to raise_error(described_class::FailedToPersistDataError)
expect(build_trace_chunk.redis?).to be_truthy
expect(Ci::BuildTraceChunks::Redis.new.data(build_trace_chunk)).to eq(data)
expect(build_trace_chunk.data_store).to eq(data_store.to_s)
expect(redis_class.new.data(build_trace_chunk)).to eq(data)
expect(Ci::BuildTraceChunks::Database.new.data(build_trace_chunk)).to be_nil
expect(Ci::BuildTraceChunks::Fog.new.data(build_trace_chunk)).to be_nil
end
@ -810,7 +832,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
shared_examples_for 'deletes all build_trace_chunk and data in redis' do
it 'deletes all build_trace_chunk and data in redis', :sidekiq_might_not_need_inline do
Gitlab::Redis::SharedState.with do |redis|
redis_instance.with do |redis|
expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(3)
end
@ -820,7 +842,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
expect(described_class.count).to eq(0)
Gitlab::Redis::SharedState.with do |redis|
redis_instance.with do |redis|
expect(redis.scan_each(match: "gitlab:ci:trace:*:chunks:*").to_a.size).to eq(0)
end
end
@ -902,4 +924,38 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
end
end
describe '#live?' do
subject { build_trace_chunk.live? }
where(:data_store, :value) do
[
[:redis, true],
[:redis_trace_chunks, true],
[:database, false],
[:fog, false]
]
end
with_them do
it { is_expected.to eq(value) }
end
end
describe '#flushed?' do
subject { build_trace_chunk.flushed? }
where(:data_store, :value) do
[
[:redis, false],
[:redis_trace_chunks, false],
[:database, true],
[:fog, true]
]
end
with_them do
it { is_expected.to eq(value) }
end
end
end

View File

@ -5,12 +5,6 @@ require 'spec_helper'
RSpec.describe Ci::BuildTraceChunks::Database do
let(:data_store) { described_class.new }
describe '#available?' do
subject { data_store.available? }
it { is_expected.to be_truthy }
end
describe '#data' do
subject { data_store.data(model) }

View File

@ -5,12 +5,6 @@ require 'spec_helper'
RSpec.describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do
let(:data_store) { described_class.new }
describe '#available?' do
subject { data_store.available? }
it { is_expected.to be_truthy }
end
describe '#data' do
subject { data_store.data(model) }

View File

@ -161,7 +161,7 @@ RSpec.describe API::Invitations do
expect_snowplow_event(
category: 'Members::InviteService',
action: 'create_member',
label: 'api',
label: 'invitations-api',
property: 'net_new_user',
user: maintainer
)

View File

@ -265,7 +265,7 @@ RSpec.describe API::Members do
expect_snowplow_event(
category: 'Members::CreateService',
action: 'create_member',
label: 'api',
label: 'members-api',
property: 'existing_user',
user: maintainer
)
@ -322,7 +322,7 @@ RSpec.describe API::Members do
expect_snowplow_event(
category: 'Members::CreateService',
action: 'create_member',
label: 'api',
label: 'members-api',
property: 'existing_user',
user: maintainer
)

View File

@ -44,7 +44,7 @@ RSpec.describe Ci::AppendBuildTraceService do
expect(::Gitlab::ErrorTracking)
.to receive(:log_exception)
.with(anything, hash_including(chunk_index: 0, chunk_store: 'redis'))
.with(anything, hash_including(chunk_index: 0, chunk_store: 'redis_trace_chunks'))
result = described_class
.new(build, content_range: '0-128')

View File

@ -30,4 +30,12 @@ RSpec.configure do |config|
redis_queues_cleanup!
end
config.around(:each, :clean_gitlab_redis_trace_chunks) do |example|
redis_trace_chunks_cleanup!
example.run
redis_trace_chunks_cleanup!
end
end

View File

@ -17,4 +17,9 @@ module RedisHelpers
def redis_shared_state_cleanup!
Gitlab::Redis::SharedState.with(&:flushall)
end
# Usage: CI trace chunks
def redis_trace_chunks_cleanup!
Gitlab::Redis::TraceChunks.with(&:flushall)
end
end