Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-06-12 09:18:26 +00:00
parent a5efa544eb
commit bd15a45eeb
82 changed files with 1005 additions and 745 deletions

View File

@ -5,7 +5,7 @@
needs: []
.qa-preflight-job:
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:bundler-${BUNDLER_VERSION}-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}
extends:
- .preflight-job-base
- .qa-cache

View File

@ -36,7 +36,7 @@ stages:
.ruby-image:
# Because this pipeline template can be included directly in other projects,
# image path and registry needs to be defined explicitly
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:bundler-${BUNDLER_VERSION}
image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}"
.bundler-variables:
variables:

View File

@ -1,5 +1,5 @@
.qa-job-base:
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:bundler-${BUNDLER_VERSION}-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}
extends:
- .default-retry
- .qa-cache
@ -31,7 +31,6 @@
- RUBY_VERSION_DEFAULT
- RUBY_VERSION_NEXT
- RUBY_VERSION
- BUNDLER_VERSION
- DOCKER_VERSION
- BUILD_OS
- OS_VERSION

View File

@ -99,8 +99,6 @@ start-review-app-pipeline:
- OS_VERSION
- DOCKER_VERSION
- CHROME_VERSION
- BUNDLER_VERSION
# These variables are set in the pipeline schedules.
# They need to be explicitly passed on to the child pipeline.
# https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html#pass-cicd-variables-to-a-downstream-pipeline-by-using-the-variables-keyword

View File

@ -180,8 +180,8 @@
# Changes patterns #
####################
.ci-patterns: &ci-patterns
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
- "{,jh/}.gitlab-ci.yml"
- "{,jh/}.gitlab/ci/**/*"
- "scripts/rspec_helpers.sh"
.ci-build-images-patterns: &ci-build-images-patterns
@ -340,8 +340,8 @@
- "{,ee/,jh/}{bin,config,db,elastic,gems,generator_templates,lib}/**/*"
- "{,ee/,jh/}spec/**/*"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
- "{,jh/}.gitlab-ci.yml"
- "{,jh/}.gitlab/ci/**/*"
- "*_VERSION"
- "scripts/rspec_helpers.sh"
# Mapped patterns (see tests.yml)
@ -455,8 +455,8 @@
# Auto-generated files
- "doc/api/graphql/reference/*"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
- "{,jh/}.gitlab-ci.yml"
- "{,jh/}.gitlab/ci/**/*"
# Mapped patterns (see tests.yml)
- "data/whats_new/*.yml"
- "doc/index.md"
@ -481,8 +481,8 @@
# Auto-generated files
- "doc/api/graphql/reference/*"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
- "{,jh/}.gitlab-ci.yml"
- "{,jh/}.gitlab/ci/**/*"
# Mapped patterns (see tests.yml)
- "data/whats_new/*.yml"
- "doc/index.md"
@ -514,8 +514,8 @@
# Auto-generated files
- "doc/api/graphql/reference/*"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
- "{,jh/}.gitlab-ci.yml"
- "{,jh/}.gitlab/ci/**/*"
# Mapped patterns (see tests.yml)
- "data/whats_new/*.yml"
- "doc/index.md"
@ -543,8 +543,8 @@
# Auto-generated files
- "doc/api/graphql/reference/*"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
- "{,jh/}.gitlab-ci.yml"
- "{,jh/}.gitlab/ci/**/*"
# Backstage changes
- "Dangerfile"
- "danger/**/*"
@ -582,8 +582,8 @@
# Auto-generated files
- "doc/api/graphql/reference/*"
# CI changes
- ".gitlab-ci.yml"
- ".gitlab/ci/**/*"
- "{,jh/}.gitlab-ci.yml"
- "{,jh/}.gitlab/ci/**/*"
# Mapped patterns (see tests.yml)
- "data/whats_new/*.yml"
- "doc/index.md"

View File

@ -11,7 +11,7 @@ workflow:
- when: always
.cng-base:
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:bundler-${BUNDLER_VERSION}-git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-kubectl-1.23-helm-3.14-kind-0.20
image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-kubectl-1.23-helm-3.14-kind-0.20"
stage: test
extends:
- .qa-cache
@ -109,7 +109,6 @@ download-knapsack-report:
cng-instance:
extends: .cng-base
variables:
QA_SCENARIO: Test::Instance::All
DEPLOYMENT_TYPE: kind
parallel: 5
allow_failure: true
@ -118,8 +117,8 @@ cng-instance:
cng-qa-min-redis-version:
extends: .cng-base
variables:
QA_SCENARIO: Test::Instance::Smoke
DEPLOYMENT_TYPE: kind
QA_RSPEC_TAGS: --tag health_check
before_script:
- |
redis_version=$(awk -F "=" "/MIN_REDIS_VERSION =/ {print \$2}" $CI_PROJECT_DIR/lib/system_check/app/redis_version_check.rb | sed "s/['\" ]//g")

View File

@ -43,7 +43,7 @@ include:
- mv $CI_BUILDS_DIR/*.log $CI_PROJECT_DIR/
.gdk-qa-base:
image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:bundler-${BUNDLER_VERSION}-git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23
image: "${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/${BUILD_OS}-${OS_VERSION}-ruby-${RUBY_VERSION}:git-2.36-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23"
extends:
- .qa-cache
- .default-retry

View File

@ -5,7 +5,6 @@ variables:
CHROME_VERSION: "123"
DOCKER_VERSION: "24.0.5"
RUBYGEMS_VERSION: "3.4"
BUNDLER_VERSION: "2.5"
GO_VERSION: "1.22"
NODE_VERSION: "20.12"
RUST_VERSION: "1.73"

View File

@ -13,7 +13,6 @@ import {
ISSUABLE_CHANGE_LABEL,
ISSUABLE_COMMENT_OR_REPLY,
ISSUABLE_EDIT_DESCRIPTION,
MR_COPY_SOURCE_BRANCH_NAME,
ISSUABLE_COPY_REF,
} from './keybindings';
@ -43,7 +42,6 @@ export default class ShortcutsIssuable {
[ISSUABLE_CHANGE_LABEL, () => ShortcutsIssuable.openSidebarDropdown('labels')],
[ISSUABLE_COMMENT_OR_REPLY, ShortcutsIssuable.replyWithSelectedText],
[ISSUABLE_EDIT_DESCRIPTION, ShortcutsIssuable.editIssue],
[MR_COPY_SOURCE_BRANCH_NAME, () => this.copyBranchName()],
[ISSUABLE_COPY_REF, () => this.copyIssuableRef()],
]);
@ -166,17 +164,6 @@ export default class ShortcutsIssuable {
return false;
}
async copyBranchName() {
const button = document.querySelector('.js-source-branch-copy');
const branchName = button?.dataset.clipboardText;
if (branchName) {
this.branchInMemoryButton.dataset.clipboardText = branchName;
this.branchInMemoryButton.dispatchEvent(new CustomEvent('click'));
}
}
async copyIssuableRef() {
const refButton = document.querySelector('.js-copy-reference');
const copiedRef = refButton?.dataset.clipboardText;

View File

@ -12,7 +12,6 @@ import {
OPERATOR_AFTER,
OPERATOR_BEFORE,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_MR_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_CONTACT,
@ -215,26 +214,6 @@ export const filtersMap = {
},
},
},
[TOKEN_TYPE_MR_ASSIGNEE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'assigneeUsername',
[SPECIAL_FILTER]: 'assigneeWildcardId',
[ALTERNATIVE_FILTER]: 'assigneeId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'mr_assignee_username',
[SPECIAL_FILTER]: 'mr_assignee_id',
[ALTERNATIVE_FILTER]: 'mr_assignee_username',
},
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[mr_assignee_username]',
},
[OPERATOR_OR]: {
[NORMAL_FILTER]: 'or[mr_assignee_username]',
},
},
},
[TOKEN_TYPE_ASSIGNEE]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'assigneeUsernames',

View File

@ -160,14 +160,14 @@ export default {
@disappear="setStickyHeaderVisible(true)"
>
<div
class="issue-sticky-header merge-request-sticky-header gl-fixed gl-bg-white gl-hidden md:gl-flex gl-flex-direction-column gl-justify-content-end gl-border-b"
class="issue-sticky-header merge-request-sticky-header gl-fixed gl-bg-white gl-hidden md:gl-flex gl-flex-col gl-justify-end gl-border-b"
:class="{ 'gl-invisible': !isStickyHeaderVisible }"
>
<div
class="issue-sticky-header-text gl-display-flex gl-flex-direction-column gl-align-items-center gl-mx-auto gl-w-full"
class="issue-sticky-header-text gl-flex gl-flex-col gl-items-center gl-mx-auto gl-w-full"
:class="{ 'container-limited': !isFluidLayout }"
>
<div class="gl-w-full gl-display-flex gl-align-items-center gl-gap-2">
<div class="gl-w-full gl-flex gl-items-center gl-gap-2">
<status-badge :issuable-type="$options.TYPE_MERGE_REQUEST" :state="badgeState.state" />
<imported-badge v-if="isImported" :importable-type="$options.TYPE_MERGE_REQUEST" />
<a
@ -175,22 +175,23 @@ export default {
href="#top"
class="gl-hidden lg:gl-block gl-font-bold gl-overflow-hidden gl-whitespace-nowrap gl-text-overflow-ellipsis gl-my-0 gl-ml-1 gl-mr-2 gl-text-black-normal"
></a>
<div class="gl-display-flex gl-align-items-center">
<div class="gl-flex gl-items-center">
<gl-sprintf :message="__('%{source} %{copyButton} into %{target}')">
<template #copyButton>
<clipboard-button
v-gl-tooltip.bottom.html="copySourceBranchTooltip"
:title="copySourceBranchTooltip"
:text="getNoteableData.source_branch"
size="small"
category="tertiary"
class="gl-m-0! gl-mx-1! js-source-branch-copy gl-align-self-center"
class="gl-mx-1"
/>
</template>
<template #source>
<gl-link
:title="getNoteableData.source_branch"
:href="getNoteableData.source_branch_path"
class="gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-text-truncate gl-max-w-26"
class="gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-mt-2 gl-truncate gl-max-w-26"
data-testid="source-branch"
>
<span
@ -208,7 +209,7 @@ export default {
<gl-link
:title="getNoteableData.target_branch"
:href="getNoteableData.target_branch_path"
class="gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-text-truncate gl-max-w-26 gl-ml-2"
class="gl-text-blue-500! gl-font-monospace gl-bg-blue-50 gl-rounded-base gl-font-sm gl-px-2 gl-mt-2 gl-truncate gl-max-w-26 gl-ml-2"
>
{{ getNoteableData.target_branch }}
</gl-link>
@ -216,21 +217,16 @@ export default {
</gl-sprintf>
</div>
</div>
<div class="gl-w-full gl-display-flex">
<div class="gl-w-full gl-flex">
<ul
class="merge-request-tabs nav-tabs nav nav-links gl-display-flex gl-flex-nowrap gl-m-0 gl-p-0 gl-border-b-0"
class="merge-request-tabs nav-tabs nav nav-links gl-flex gl-flex-nowrap gl-m-0 gl-p-0 gl-border-b-0"
>
<li
v-for="(tab, index) in tabs"
:key="tab[0]"
:class="{ active: activeTab === tab[0] }"
>
<gl-link
:href="tab[2]"
:data-action="tab[0]"
class="!gl-outline-none gl-py-4!"
@click="visitTab"
>
<gl-link :href="tab[2]" :data-action="tab[0]" class="!gl-py-4" @click="visitTab">
{{ tab[1] }}
<gl-badge variant="muted" size="sm">
<template v-if="index === 0 && discussionCounter !== 0">
@ -243,12 +239,9 @@ export default {
</gl-link>
</li>
</ul>
<div class="gl-hidden lg:gl-flex gl-align-items-center gl-ml-auto">
<div class="gl-hidden lg:gl-flex gl-items-center gl-ml-auto">
<discussion-counter :blocks-merge="blocksMerge" hide-options />
<div
v-if="isSignedIn"
:class="{ 'gl-display-flex gl-gap-3': isNotificationsTodosButtons }"
>
<div v-if="isSignedIn" :class="{ 'gl-flex gl-gap-3': isNotificationsTodosButtons }">
<todo-widget
:issuable-id="issuableId"
:issuable-iid="issuableIid"

View File

@ -24,7 +24,7 @@ import {
TOKEN_TITLE_SOURCE_BRANCH,
TOKEN_TYPE_SOURCE_BRANCH,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TYPE_MR_ASSIGNEE,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TITLE_MILESTONE,
TOKEN_TYPE_MILESTONE,
} from '~/vue_shared/components/filtered_search_bar/constants';
@ -178,7 +178,7 @@ export default {
return [
{
type: TOKEN_TYPE_MR_ASSIGNEE,
type: TOKEN_TYPE_ASSIGNEE,
title: TOKEN_TITLE_ASSIGNEE,
icon: 'user',
token: UserToken,

View File

@ -7,7 +7,8 @@ query getMergeRequests(
$fullPath: ID!
$sort: MergeRequestSort
$state: MergeRequestState
$assigneeUsername: String
$assigneeUsernames: String
$assigneeWildcardId: AssigneeWildcardId
$authorUsername: String
$draft: Boolean
$milestoneTitle: String
@ -24,7 +25,8 @@ query getMergeRequests(
mergeRequests(
sort: $sort
state: $state
assigneeUsername: $assigneeUsername
assigneeUsername: $assigneeUsernames
assigneeWildcardId: $assigneeWildcardId
authorUsername: $authorUsername
draft: $draft
milestoneTitle: $milestoneTitle

View File

@ -1,5 +1,7 @@
query getMergeRequestsCount(
$fullPath: ID!
$assigneeWildcardId: AssigneeWildcardId
$assigneeUsernames: String
$milestoneTitle: String
$milestoneWildcardId: MilestoneWildcardId
) {
@ -7,6 +9,8 @@ query getMergeRequestsCount(
id
openedMergeRequests: mergeRequests(
state: opened
assigneeUsername: $assigneeUsernames
assigneeWildcardId: $assigneeWildcardId
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
) {
@ -14,6 +18,8 @@ query getMergeRequestsCount(
}
mergedMergeRequests: mergeRequests(
state: merged
assigneeUsername: $assigneeUsernames
assigneeWildcardId: $assigneeWildcardId
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
) {
@ -21,6 +27,8 @@ query getMergeRequestsCount(
}
closedMergeRequests: mergeRequests(
state: closed
assigneeUsername: $assigneeUsernames
assigneeWildcardId: $assigneeWildcardId
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
) {
@ -28,6 +36,8 @@ query getMergeRequestsCount(
}
allMergeRequests: mergeRequests(
state: all
assigneeUsername: $assigneeUsernames
assigneeWildcardId: $assigneeWildcardId
milestoneTitle: $milestoneTitle
milestoneWildcardId: $milestoneWildcardId
) {

View File

@ -39,20 +39,20 @@ export default {
</script>
<template>
<div class="revision-card gl-flex-basis-half">
<div class="revision-card gl-flex-basis-half gl-min-w-0">
<h2 class="gl-font-base gl-mt-0">
{{ revisionText }}
</h2>
<div class="sm:gl-flex gl-align-items-center gl-gap-3">
<div class="gl-flex gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3">
<repo-dropdown
class="gl-sm-w-half"
class="gl-flex-basis-half gl-min-w-0 gl-max-w-full"
:params-name="paramsName"
:projects="projects"
:selected-project="selectedProject"
v-on="$listeners"
/>
<revision-dropdown
class="gl-sm-w-half gl-mt-3 gl-sm-mt-0"
class="gl-flex-basis-half gl-min-w-0 gl-max-w-full"
:refs-project-path="refsProjectPath"
:params-name="paramsName"
:params-branch="paramsBranch"

View File

@ -1,12 +1,17 @@
<script>
import { GlCollapsibleListbox, GlSprintf } from '@gitlab/ui';
import { InternalEvents } from '~/tracking';
import { s__ } from '~/locale';
import { EVENT_CLICK_COMMANDS_SUB_MENU_IN_COMMAND_PALETTE } from '~/super_sidebar/components/global_search/tracking_constants';
const trackingMixin = InternalEvents.mixin();
export default {
name: 'CommandsOverviewDropdown',
components: { GlCollapsibleListbox, GlSprintf },
mixins: [trackingMixin],
i18n: {
header: s__('GlobalSearch|Im looking for'),
header: s__("GlobalSearch|I'm looking for"),
button: s__('GlobalSearch|Commands %{link1Start}⌘%{link1End} %{link2Start}k%{link2End}'),
},
props: {
@ -26,6 +31,7 @@ export default {
this.$refs.commandsDropdown.close();
},
},
EVENT_CLICK_COMMANDS_SUB_MENU_IN_COMMAND_PALETTE,
};
</script>
@ -37,6 +43,7 @@ export default {
:header-text="$options.i18n.header"
category="tertiary"
@select="emitSelected"
@shown="trackEvent($options.EVENT_CLICK_COMMANDS_SUB_MENU_IN_COMMAND_PALETTE)"
>
<template #toggle>
<button class="gl-border-0 gl-rounded-base">

View File

@ -80,3 +80,6 @@ export const OVERLAY_PROJECT = s__('GlobalSearch|Go to project %{kbdStart}↵%{k
export const OVERLAY_FILE = s__('GlobalSearch|Go to file %{kbdStart}↵%{kbdEnd}');
export const OVERLAY_GOTO = s__('GlobalSearch|Go to %{kbdStart}↵%{kbdEnd}');
export const FREQUENTLY_VISITED_PROJECTS_HANDLE = 'FREQUENTLY_VISITED_PROJECTS_HANDLE';
export const FREQUENTLY_VISITED_GROUPS_HANDLE = 'FREQUENTLY_VISITED_GROUPS_HANDLE';

View File

@ -1,6 +1,7 @@
<script>
import { s__ } from '~/locale';
import currentUserFrecentGroupsQuery from '~/super_sidebar/graphql/queries/current_user_frecent_groups.query.graphql';
import { FREQUENTLY_VISITED_GROUPS_HANDLE } from '~/super_sidebar/components/global_search/command_palette/constants';
import FrequentItems from './frequent_items.vue';
export default {
@ -19,6 +20,7 @@ export default {
viewAllText: s__('Navigation|View all my groups'),
emptyStateText: s__('Navigation|Groups you visit often will appear here.'),
},
FREQUENTLY_VISITED_GROUPS_HANDLE,
};
</script>
@ -33,5 +35,6 @@ export default {
:view-all-items-path="groupsPath"
v-bind="$attrs"
v-on="$listeners"
@action="$emit('action', $options.FREQUENTLY_VISITED_GROUPS_HANDLE)"
/>
</template>

View File

@ -107,6 +107,7 @@ export default {
:key="item.forDropdown.id"
:item="item.forDropdown"
class="show-on-focus-or-hover--context show-hover-layover"
@action="$emit('action')"
>
<template #list-item><frequent-item :item="item.forRenderer" /></template>
</gl-disclosure-dropdown-item>

View File

@ -1,6 +1,7 @@
<script>
import { s__ } from '~/locale';
import currentUserFrecentProjectsQuery from '~/super_sidebar/graphql/queries/current_user_frecent_projects.query.graphql';
import { FREQUENTLY_VISITED_PROJECTS_HANDLE } from '~/super_sidebar/components/global_search/command_palette/constants';
import FrequentItems from './frequent_items.vue';
export default {
@ -19,6 +20,7 @@ export default {
viewAllText: s__('Navigation|View all my projects'),
emptyStateText: s__('Navigation|Projects you visit often will appear here.'),
},
FREQUENTLY_VISITED_PROJECTS_HANDLE,
};
</script>
@ -33,5 +35,6 @@ export default {
:view-all-items-path="projectsPath"
v-bind="$attrs"
v-on="$listeners"
@action="$emit('action', $options.FREQUENTLY_VISITED_PROJECTS_HANDLE)"
/>
</template>

View File

@ -1,13 +1,26 @@
<script>
import {
FREQUENTLY_VISITED_PROJECTS_HANDLE,
FREQUENTLY_VISITED_GROUPS_HANDLE,
} from '~/super_sidebar/components/global_search/command_palette/constants';
import {
EVENT_CLICK_FREQUENT_GROUP_IN_COMMAND_PALETTE,
EVENT_CLICK_FREQUENT_PROJECT_IN_COMMAND_PALETTE,
} from '~/super_sidebar/components/global_search/tracking_constants';
import { InternalEvents } from '~/tracking';
import DefaultPlaces from './global_search_default_places.vue';
import DefaultIssuables from './global_search_default_issuables.vue';
import FrequentGroups from './frequent_groups.vue';
import FrequentProjects from './frequent_projects.vue';
const components = [DefaultPlaces, FrequentProjects, FrequentGroups, DefaultIssuables];
const trackingMixin = InternalEvents.mixin();
export default {
name: 'GlobalSearchDefaultItems',
mixins: [trackingMixin],
data() {
return {
// The components here are expected to:
@ -36,6 +49,21 @@ export default {
class: 'gl-mt-3',
};
},
trackItems(type) {
switch (type) {
case FREQUENTLY_VISITED_PROJECTS_HANDLE: {
this.trackEvent(EVENT_CLICK_FREQUENT_PROJECT_IN_COMMAND_PALETTE);
break;
}
case FREQUENTLY_VISITED_GROUPS_HANDLE: {
this.trackEvent(EVENT_CLICK_FREQUENT_GROUP_IN_COMMAND_PALETTE);
break;
}
default: {
break;
}
}
},
},
};
</script>
@ -48,6 +76,7 @@ export default {
:key="name"
v-bind="attrs(index)"
@nothing-to-render="remove(name)"
@action="trackItems"
/>
</ul>
</template>

View File

@ -32,3 +32,9 @@ export const EVENT_CLICK_RECENT_EPIC_RESULT_IN_COMMAND_PALETTE =
export const EVENT_CLICK_RECENT_MERGE_REQUEST_RESULT_IN_COMMAND_PALETTE =
'click_recent_merge_request_result_in_command_palette';
export const EVENT_CLICK_USER_RESULT_IN_COMMAND_PALETTE = 'click_user_result_in_command_palette';
export const EVENT_CLICK_FREQUENT_PROJECT_IN_COMMAND_PALETTE =
'click_frequent_project_in_command_palette';
export const EVENT_CLICK_FREQUENT_GROUP_IN_COMMAND_PALETTE =
'click_frequent_group_in_command_palette';
export const EVENT_CLICK_COMMANDS_SUB_MENU_IN_COMMAND_PALETTE =
'click_commands_sub_menu_in_command_palette';

View File

@ -90,7 +90,6 @@ export const TOKEN_TITLE_CLOSED = __('Closed date');
export const TOKEN_TYPE_APPROVED_BY = 'approved-by';
export const TOKEN_TYPE_MERGE_USER = 'merge-user';
export const TOKEN_TYPE_ASSIGNEE = 'assignee';
export const TOKEN_TYPE_MR_ASSIGNEE = 'mr-assignee';
export const TOKEN_TYPE_AUTHOR = 'author';
export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
export const TOKEN_TYPE_CONTACT = 'contact';

View File

@ -17,6 +17,7 @@ module ResolvesMergeRequests
end
rewrite_param_name(args, :reviewer_wildcard_id, :reviewer_id)
rewrite_param_name(args, :assignee_wildcard_id, :assignee_id)
mr_finder = MergeRequestsFinder.new(current_user, args.compact)
finder = Gitlab::Graphql::Loaders::IssuableLoader.new(mr_parent, mr_finder)

View File

@ -13,6 +13,9 @@ module Resolvers
argument :assignee_username, GraphQL::Types::String,
required: false,
description: 'Username of the assignee.'
argument :assignee_wildcard_id, ::Types::AssigneeWildcardIdEnum,
required: false,
description: 'Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername.'
end
def self.accept_author
@ -126,6 +129,8 @@ module Resolvers
description: 'Title of the milestone.'
end
validates mutually_exclusive: [:assignee_username, :assignee_wildcard_id]
validates mutually_exclusive: [:reviewer_username, :reviewer_wildcard_id]
validates mutually_exclusive: [:milestone_title, :milestone_wildcard_id]
def self.single

View File

@ -308,7 +308,7 @@ module MergeRequestsHelper
copy_action_description = _('Copy branch name')
copy_action_shortcut = 'b'
copy_button_title = "#{copy_action_description} <kbd class='flat ml-1' aria-hidden=true>#{copy_action_shortcut}</kbd>"
copy_button = clipboard_button(text: merge_request.source_branch, title: copy_button_title, aria_keyshortcuts: copy_action_shortcut, aria_label: copy_action_description, class: '!gl-hidden md:!gl-inline-block js-source-branch-copy gl-mx-1')
copy_button = clipboard_button(text: merge_request.source_branch, title: copy_button_title, aria_keyshortcuts: copy_action_shortcut, aria_label: copy_action_description, class: '!gl-hidden md:!gl-inline-block gl-mx-1')
target_branch = link_to merge_request.target_branch, project_tree_path(merge_request.target_project, merge_request.target_branch), title: merge_request.target_branch, class: 'ref-container gl-display-inline-block gl-text-truncate gl-max-w-26 gl-mx-2'

View File

@ -14,14 +14,14 @@
.detail-page-header.border-bottom-0.gl-block.gl-pt-5{ class: "sm:!gl-flex #{'is-merge-request' if !fluid_layout}" }
.detail-page-header-body
%h1.title.page-title.gl-font-size-h-display.gl-my-0.gl-display-inline-block.gl-flex-grow-1.gl-break-anywhere{ data: { testid: 'title-content' } }
%h1.title.gl-heading-1.gl-my-0.gl-block.gl-grow.gl-break-anywhere{ class: '!gl-m-0', data: { testid: 'title-content' } }
= markdown_field(@merge_request, :title)
- unless hide_gutter_toggle
%div
= render Pajamas::ButtonComponent.new(icon: "chevron-double-lg-left", button_options: { class: "btn-icon gl-float-right gl-block gutter-toggle issuable-gutter-toggle js-sidebar-toggle sm:!gl-hidden" })
.detail-page-header-actions.gl-align-self-start.is-merge-request.js-issuable-actions.gl-display-flex
.detail-page-header-actions.gl-self-start.is-merge-request.js-issuable-actions.gl-flex.gl-mt-1
- if can_update_merge_request
- edit_action_description = _('Edit merge request')
- edit_action_shortcut = 'e'
@ -29,7 +29,7 @@
= render Pajamas::ButtonComponent.new(href: edit_project_merge_request_path(@project, @merge_request), button_options: { aria: {label: edit_action_description, keyshortcuts: edit_action_shortcut}, class: "gl-hidden sm:gl-block gl-align-self-start has-tooltip js-issuable-edit", data: { html: "true", testid: "edit-title-button" }, title: edit_button_title }) do
= _('Edit')
.gl-display-flex.gl-flex-direction-column.gl-sm-flex-direction-row.gl-gap-3.gl-w-full.gl-sm-w-auto.gl-mt-2.gl-sm-mt-0
.gl-flex.gl-flex-col.sm:gl-flex-row.gl-gap-3.gl-w-full.sm:gl-w-auto.gl-mt-2.sm:gl-mt-0
- if @merge_request.source_project
= render 'projects/merge_requests/code_dropdown'

View File

@ -203,6 +203,10 @@ module ApplicationWorker
end
end
def with_ip_address_state
set(ip_address_state: ::Gitlab::IpAddressState.current)
end
private
def do_push_bulk(args_list)

View File

@ -0,0 +1,17 @@
---
description: User clicks commands sub-menu in footer
internal_events: true
action: click_commands_sub_menu_in_command_palette
identifiers:
- namespace
- user
product_group: global_search
milestone: '17.1'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154150
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate

View File

@ -0,0 +1,17 @@
---
description: User clicks a group in the frequent groups section
internal_events: true
action: click_frequent_group_in_command_palette
identifiers:
- namespace
- user
product_group: global_search
milestone: '17.1'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154150
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate

View File

@ -0,0 +1,17 @@
---
description: User clicks a project in the frequent projects section
internal_events: true
action: click_frequent_project_in_command_palette
identifiers:
- namespace
- user
product_group: global_search
milestone: '17.1'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154150
distributions:
- ce
- ee
tiers:
- free
- premium
- ultimate

View File

@ -784,6 +784,9 @@ Gitlab.ee do
Settings.cron_jobs['elastic_index_bulk_cron_worker'] ||= {}
Settings.cron_jobs['elastic_index_bulk_cron_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['elastic_index_bulk_cron_worker']['job_class'] ||= 'ElasticIndexBulkCronWorker'
Settings.cron_jobs['elastic_index_embedding_bulk_cron_worker'] ||= {}
Settings.cron_jobs['elastic_index_embedding_bulk_cron_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['elastic_index_embedding_bulk_cron_worker']['job_class'] ||= 'Search::ElasticIndexEmbeddingBulkCronWorker'
Settings.cron_jobs['elastic_index_initial_bulk_cron_worker'] ||= {}
Settings.cron_jobs['elastic_index_initial_bulk_cron_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['elastic_index_initial_bulk_cron_worker']['job_class'] ||= 'ElasticIndexInitialBulkCronWorker'

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_click_commands_sub_menu_in_command_palette_monthly
description: Monthly count of unique users clicking commands sub-menu in footer
product_group: global_search
performance_indicator_type: []
value_type: number
status: active
milestone: '17.1'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154150
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: click_commands_sub_menu_in_command_palette
unique: user.id

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_click_frequent_group_in_command_palette_monthly
description: Monthly count of unique users clicking a group in the frequent groups section
product_group: global_search
performance_indicator_type: []
value_type: number
status: active
milestone: '17.1'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154150
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: click_frequent_group_in_command_palette
unique: user.id

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_click_frequent_project_in_command_palette_monthly
description: Monthly count of unique users clicking a project in the frequent projects section
product_group: global_search
performance_indicator_type: []
value_type: number
status: active
milestone: '17.1'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154150
time_frame: 28d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: click_frequent_project_in_command_palette
unique: user.id

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_click_commands_sub_menu_in_command_palette_weekly
description: Weekly count of unique users who click commands sub-menu in footer
product_group: global_search
performance_indicator_type: []
value_type: number
status: active
milestone: '17.1'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154150
time_frame: 7d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: click_commands_sub_menu_in_command_palette
unique: user.id

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_click_frequent_group_in_command_palette_weekly
description: Weekly count of unique users who click a group in the frequent groups section
product_group: global_search
performance_indicator_type: []
value_type: number
status: active
milestone: '17.1'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154150
time_frame: 7d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: click_frequent_group_in_command_palette
unique: user.id

View File

@ -0,0 +1,22 @@
---
key_path: redis_hll_counters.count_distinct_user_id_from_click_frequent_project_in_command_palette_weekly
description: Weekly count of unique users who click a project in the frequent projects section
product_group: global_search
performance_indicator_type: []
value_type: number
status: active
milestone: '17.1'
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154150
time_frame: 7d
data_source: internal_events
data_category: optional
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
events:
- name: click_frequent_project_in_command_palette
unique: user.id

View File

@ -0,0 +1,9 @@
---
migration_job_name: BackfillStatusCheckResponsesProjectId
description: Backfills sharding key `status_check_responses.project_id` from `merge_requests`.
feature_category: compliance_management
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156033
milestone: '17.1'
queued_migration_version: 20240611142352
finalize_after: '2024-07-22'
finalized_by: # version of the migration that finalized this BBM

View File

@ -19,3 +19,4 @@ desired_sharding_key:
table: merge_requests
sharding_key: target_project_id
belongs_to: merge_request
desired_sharding_key_migration_job_name: BackfillStatusCheckResponsesProjectId

View File

@ -10,4 +10,5 @@ milestone: '13.0'
gitlab_schema: gitlab_main_cell
allow_cross_foreign_keys:
- gitlab_main_clusterwide
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/457095
sharding_key:
organization_id: organizations

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
class AddOrganizationToVulnerabilityExports < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.1'
INDEX_NAME = 'index_vulnerability_exports_on_organization_id'
def up
with_lock_retries do
add_column :vulnerability_exports, :organization_id, :bigint, null: false,
default: Organizations::Organization::DEFAULT_ORGANIZATION_ID,
if_not_exists: true
end
add_concurrent_foreign_key :vulnerability_exports, :organizations, column: :organization_id, on_delete: :cascade
add_concurrent_index :vulnerability_exports, :organization_id, name: INDEX_NAME
end
def down
with_lock_retries do
remove_foreign_key :vulnerability_exports, column: :organization_id
end
remove_concurrent_index_by_name :vulnerability_exports, INDEX_NAME
with_lock_retries do
remove_column :vulnerability_exports, :organization_id, if_exists: true
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
# See https://docs.gitlab.com/ee/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class UpdateUniqueUserNamespaceIndexOnMemberApprovals < Gitlab::Database::Migration[2.2]
milestone '17.1'
disable_ddl_transaction!
OLD_INDEX_NAME = 'unique_index_member_approvals_on_pending_status'
NEW_INDEX_NAME = 'unique_idx_member_approvals_on_pending_status'
def up
add_concurrent_index :member_approvals, [:user_id, :member_namespace_id],
unique: true, where: "status = 0", name: NEW_INDEX_NAME
remove_concurrent_index_by_name :member_approvals, OLD_INDEX_NAME
end
def down
add_concurrent_index :member_approvals, [:user_id, :member_namespace_id, :new_access_level, :member_role_id],
unique: true, where: "status = 0", name: OLD_INDEX_NAME
remove_concurrent_index_by_name :member_approvals, NEW_INDEX_NAME
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddProjectIdToStatusCheckResponses < Gitlab::Database::Migration[2.2]
milestone '17.1'
def change
add_column :status_check_responses, :project_id, :bigint
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class IndexStatusCheckResponsesOnProjectId < Gitlab::Database::Migration[2.2]
milestone '17.1'
disable_ddl_transaction!
INDEX_NAME = 'index_status_check_responses_on_project_id'
def up
add_concurrent_index :status_check_responses, :project_id, name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :status_check_responses, INDEX_NAME
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
class AddStatusCheckResponsesProjectIdFk < Gitlab::Database::Migration[2.2]
milestone '17.1'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :status_check_responses, :projects, column: :project_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :status_check_responses, column: :project_id
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
class AddStatusCheckResponsesProjectIdTrigger < Gitlab::Database::Migration[2.2]
milestone '17.1'
def up
install_sharding_key_assignment_trigger(
table: :status_check_responses,
sharding_key: :project_id,
parent_table: :merge_requests,
parent_sharding_key: :target_project_id,
foreign_key: :merge_request_id
)
end
def down
remove_sharding_key_assignment_trigger(
table: :status_check_responses,
sharding_key: :project_id,
parent_table: :merge_requests,
parent_sharding_key: :target_project_id,
foreign_key: :merge_request_id
)
end
end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class QueueBackfillStatusCheckResponsesProjectId < Gitlab::Database::Migration[2.2]
milestone '17.1'
restrict_gitlab_migration gitlab_schema: :gitlab_main_cell
MIGRATION = "BackfillStatusCheckResponsesProjectId"
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 1000
SUB_BATCH_SIZE = 100
def up
queue_batched_background_migration(
MIGRATION,
:status_check_responses,
:id,
:project_id,
:merge_requests,
:target_project_id,
:merge_request_id,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE
)
end
def down
delete_batched_background_migration(
MIGRATION,
:status_check_responses,
:id,
[
:project_id,
:merge_requests,
:target_project_id,
:merge_request_id
]
)
end
end

View File

@ -0,0 +1 @@
eaab626b81ea7df21bdbbcf3f0da019bce66d6bae83a2c18fcec732d22d52c7d

View File

@ -0,0 +1 @@
f2592bf6d74deffbef5626e33856f33af256a3a5f3074e8cfa31eb6409362f9b

View File

@ -0,0 +1 @@
ec17602042e8e11da5f1faa7b58659fb455dbf1a2dfdd8cfae90fac5f7ee87f6

View File

@ -0,0 +1 @@
2c2d509c9019c35b59e2231f8c473fd3a0af60bcebf45aa4c0bfa10a590614e5

View File

@ -0,0 +1 @@
aea0e6c1b36b4ec32f8bbb22c1e2eab6a4f7a060f486382267fc16e355dd72c5

View File

@ -0,0 +1 @@
4844652782e0966eeb1ce935a21a06291f534623bf8c91f9c2f8c171bd9d1645

View File

@ -0,0 +1 @@
5f92f0daf25ba601f64916028a9bb980d37a28dc7b0c9c513d1e8bb8aad706b3

View File

@ -733,6 +733,22 @@ RETURN NEW;
END
$$;
CREATE FUNCTION trigger_05ce163deddf() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW."project_id" IS NULL THEN
SELECT "target_project_id"
INTO NEW."project_id"
FROM "merge_requests"
WHERE "merge_requests"."id" = NEW."merge_request_id";
END IF;
RETURN NEW;
END
$$;
CREATE FUNCTION trigger_0da002390fdc() RETURNS trigger
LANGUAGE plpgsql
AS $$
@ -17221,7 +17237,8 @@ CREATE TABLE status_check_responses (
external_status_check_id bigint NOT NULL,
status smallint DEFAULT 0 NOT NULL,
retried_at timestamp with time zone,
created_at timestamp with time zone DEFAULT now() NOT NULL
created_at timestamp with time zone DEFAULT now() NOT NULL,
project_id bigint
);
CREATE SEQUENCE status_check_responses_id_seq
@ -18301,7 +18318,8 @@ CREATE TABLE vulnerability_exports (
author_id bigint NOT NULL,
file_store integer,
format smallint DEFAULT 0 NOT NULL,
group_id integer
group_id integer,
organization_id bigint DEFAULT 1 NOT NULL
);
CREATE SEQUENCE vulnerability_exports_id_seq
@ -28365,6 +28383,8 @@ CREATE INDEX index_status_check_responses_on_external_status_check_id ON status_
CREATE INDEX index_status_check_responses_on_merge_request_id ON status_check_responses USING btree (merge_request_id);
CREATE INDEX index_status_check_responses_on_project_id ON status_check_responses USING btree (project_id);
CREATE UNIQUE INDEX index_status_page_published_incidents_on_issue_id ON status_page_published_incidents USING btree (issue_id);
CREATE INDEX index_status_page_settings_on_project_id ON status_page_settings USING btree (project_id);
@ -28713,6 +28733,8 @@ CREATE INDEX index_vulnerability_exports_on_file_store ON vulnerability_exports
CREATE INDEX index_vulnerability_exports_on_group_id_not_null ON vulnerability_exports USING btree (group_id) WHERE (group_id IS NOT NULL);
CREATE INDEX index_vulnerability_exports_on_organization_id ON vulnerability_exports USING btree (organization_id);
CREATE INDEX index_vulnerability_exports_on_project_id_not_null ON vulnerability_exports USING btree (project_id) WHERE (project_id IS NOT NULL);
CREATE INDEX index_vulnerability_external_issue_links_on_author_id ON vulnerability_external_issue_links USING btree (author_id);
@ -29147,6 +29169,8 @@ CREATE UNIQUE INDEX unique_external_audit_event_destination_namespace_id_and_nam
CREATE UNIQUE INDEX unique_google_cloud_logging_configurations_on_namespace_id ON audit_events_google_cloud_logging_configurations USING btree (namespace_id, google_project_id_name, log_id_name);
CREATE UNIQUE INDEX unique_idx_member_approvals_on_pending_status ON member_approvals USING btree (user_id, member_namespace_id) WHERE (status = 0);
CREATE UNIQUE INDEX unique_idx_namespaces_storage_limit_exclusions_on_namespace_id ON namespaces_storage_limit_exclusions USING btree (namespace_id);
CREATE UNIQUE INDEX unique_import_source_users_source_identifier_and_import_source ON import_source_users USING btree (source_user_identifier, namespace_id, source_hostname, import_type);
@ -29157,8 +29181,6 @@ CREATE UNIQUE INDEX unique_index_for_credit_card_validation_payment_method_xid O
CREATE UNIQUE INDEX unique_index_for_project_pages_unique_domain ON project_settings USING btree (pages_unique_domain) WHERE (pages_unique_domain IS NOT NULL);
CREATE UNIQUE INDEX unique_index_member_approvals_on_pending_status ON member_approvals USING btree (user_id, member_namespace_id, new_access_level, member_role_id) WHERE (status = 0);
CREATE UNIQUE INDEX unique_index_ml_model_metadata_name ON ml_model_metadata USING btree (model_id, name);
CREATE UNIQUE INDEX unique_index_ml_model_version_metadata_name ON ml_model_version_metadata USING btree (model_version_id, name);
@ -30787,6 +30809,8 @@ CREATE TRIGGER tags_loose_fk_trigger AFTER DELETE ON tags REFERENCING OLD TABLE
CREATE TRIGGER trigger_01b3fc052119 BEFORE INSERT OR UPDATE ON approval_merge_request_rules FOR EACH ROW EXECUTE FUNCTION trigger_01b3fc052119();
CREATE TRIGGER trigger_05ce163deddf BEFORE INSERT OR UPDATE ON status_check_responses FOR EACH ROW EXECUTE FUNCTION trigger_05ce163deddf();
CREATE TRIGGER trigger_0da002390fdc BEFORE INSERT OR UPDATE ON operations_feature_flags_issues FOR EACH ROW EXECUTE FUNCTION trigger_0da002390fdc();
CREATE TRIGGER trigger_0e13f214e504 BEFORE INSERT OR UPDATE ON merge_request_assignment_events FOR EACH ROW EXECUTE FUNCTION trigger_0e13f214e504();
@ -31630,6 +31654,9 @@ ALTER TABLE ONLY merge_request_review_llm_summaries
ALTER TABLE ONLY audit_events_streaming_group_namespace_filters
ADD CONSTRAINT fk_8ed182d7da FOREIGN KEY (external_streaming_destination_id) REFERENCES audit_events_group_external_streaming_destinations(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_exports
ADD CONSTRAINT fk_90e75ccdf8 FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
ALTER TABLE ONLY todos
ADD CONSTRAINT fk_91d1f47b13 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
@ -31813,6 +31840,9 @@ ALTER TABLE ONLY issues
ALTER TABLE ONLY protected_tag_create_access_levels
ADD CONSTRAINT fk_b4eb82fe3c FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY status_check_responses
ADD CONSTRAINT fk_b53bf31a72 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY compliance_framework_security_policies
ADD CONSTRAINT fk_b5df066d8f FOREIGN KEY (framework_id) REFERENCES compliance_management_frameworks(id) ON DELETE CASCADE;

View File

@ -27,7 +27,7 @@ choose one or the other; there is no particular benefit in combining them.
## Routing rules
> - [Default routing rule value](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97908) added in GitLab 15.4.
> - [Default routing rule value](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97908) introduced in GitLab 15.4.
NOTE:
Mailer jobs cannot be routed by routing rules, and always go to the

View File

@ -16069,6 +16069,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="addonuserauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="addonuserauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="addonuserauthoredmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="addonuserauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="addonuserauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
| <a id="addonuserauthoredmergerequestsdeployedafter"></a>`deployedAfter` | [`Time`](#time) | Merge requests deployed after the timestamp. |
@ -16165,6 +16166,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="addonuserreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="addonuserreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="addonuserreviewrequestedmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="addonuserreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="addonuserreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="addonuserreviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -16877,6 +16879,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="autocompleteduserauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="autocompleteduserauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="autocompleteduserauthoredmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="autocompleteduserauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="autocompleteduserauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
| <a id="autocompleteduserauthoredmergerequestsdeployedafter"></a>`deployedAfter` | [`Time`](#time) | Merge requests deployed after the timestamp. |
@ -16985,6 +16988,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="autocompleteduserreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="autocompleteduserreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="autocompleteduserreviewrequestedmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="autocompleteduserreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="autocompleteduserreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="autocompleteduserreviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -19073,6 +19077,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="currentuserauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="currentuserauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="currentuserauthoredmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="currentuserauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="currentuserauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
| <a id="currentuserauthoredmergerequestsdeployedafter"></a>`deployedAfter` | [`Time`](#time) | Merge requests deployed after the timestamp. |
@ -19169,6 +19174,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="currentuserreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="currentuserreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="currentuserreviewrequestedmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="currentuserreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="currentuserreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="currentuserreviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -22374,6 +22380,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="groupmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="groupmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="groupmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="groupmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="groupmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="groupmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -24387,6 +24394,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="mergerequestassigneeauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="mergerequestassigneeauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestassigneeauthoredmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="mergerequestassigneeauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="mergerequestassigneeauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
| <a id="mergerequestassigneeauthoredmergerequestsdeployedafter"></a>`deployedAfter` | [`Time`](#time) | Merge requests deployed after the timestamp. |
@ -24483,6 +24491,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="mergerequestassigneereviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="mergerequestassigneereviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestassigneereviewrequestedmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="mergerequestassigneereviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestassigneereviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="mergerequestassigneereviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -24727,6 +24736,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="mergerequestauthorauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="mergerequestauthorauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestauthorauthoredmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="mergerequestauthorauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="mergerequestauthorauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
| <a id="mergerequestauthorauthoredmergerequestsdeployedafter"></a>`deployedAfter` | [`Time`](#time) | Merge requests deployed after the timestamp. |
@ -24823,6 +24833,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="mergerequestauthorreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="mergerequestauthorreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestauthorreviewrequestedmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="mergerequestauthorreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestauthorreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="mergerequestauthorreviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -25114,6 +25125,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="mergerequestparticipantauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="mergerequestparticipantauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestparticipantauthoredmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="mergerequestparticipantauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="mergerequestparticipantauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
| <a id="mergerequestparticipantauthoredmergerequestsdeployedafter"></a>`deployedAfter` | [`Time`](#time) | Merge requests deployed after the timestamp. |
@ -25210,6 +25222,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="mergerequestparticipantreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="mergerequestparticipantreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestparticipantreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="mergerequestparticipantreviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -25490,6 +25503,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="mergerequestreviewerauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="mergerequestreviewerauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestreviewerauthoredmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="mergerequestreviewerauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="mergerequestreviewerauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
| <a id="mergerequestreviewerauthoredmergerequestsdeployedafter"></a>`deployedAfter` | [`Time`](#time) | Merge requests deployed after the timestamp. |
@ -25586,6 +25600,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="mergerequestreviewerreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="mergerequestreviewerreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="mergerequestreviewerreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="mergerequestreviewerreviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -28514,6 +28529,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="projectmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="projectmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="projectmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="projectmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="projectmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="projectmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -31342,6 +31358,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="usercoreauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="usercoreauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="usercoreauthoredmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="usercoreauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="usercoreauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
| <a id="usercoreauthoredmergerequestsdeployedafter"></a>`deployedAfter` | [`Time`](#time) | Merge requests deployed after the timestamp. |
@ -31438,6 +31455,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="usercorereviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="usercorereviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="usercorereviewrequestedmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="usercorereviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="usercorereviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="usercorereviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
@ -37752,6 +37770,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="userauthoredmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="userauthoredmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="userauthoredmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="userauthoredmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="userauthoredmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |
| <a id="userauthoredmergerequestsdeployedafter"></a>`deployedAfter` | [`Time`](#time) | Merge requests deployed after the timestamp. |
@ -37848,6 +37867,7 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="userreviewrequestedmergerequestsapproved"></a>`approved` | [`Boolean`](#boolean) | Limit results to approved merge requests. Available only when the feature flag `mr_approved_filter` is enabled. |
| <a id="userreviewrequestedmergerequestsassigneeusername"></a>`assigneeUsername` | [`String`](#string) | Username of the assignee. |
| <a id="userreviewrequestedmergerequestsassigneewildcardid"></a>`assigneeWildcardId` | [`AssigneeWildcardId`](#assigneewildcardid) | Filter by assignee presence. Incompatible with assigneeUsernames and assigneeUsername. |
| <a id="userreviewrequestedmergerequestsauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author. |
| <a id="userreviewrequestedmergerequestscreatedafter"></a>`createdAfter` | [`Time`](#time) | Merge requests created after the timestamp. |
| <a id="userreviewrequestedmergerequestscreatedbefore"></a>`createdBefore` | [`Time`](#time) | Merge requests created before the timestamp. |

View File

@ -49,8 +49,8 @@ For example:
1. **GraphQL and other ambiguous endpoints.**
Most endpoints have a unique sharding key: the Organization, which directly or indirectly (via a Group or Project) can be used to classify endpoints.
Some endpoints are ambiguous in their usage (they don't encode the sharding key), or the sharding key is stored deep in the payload.
Most endpoints have a unique classification key: the Organization, which directly or indirectly (via a Group or Project) can be used to classify endpoints.
Some endpoints are ambiguous in their usage (they don't encode the classification key), or the classification key is stored deep in the payload.
In these cases, we need to decide how to handle endpoints like `/api/graphql`.
1. **Small.**
@ -207,9 +207,9 @@ The Routing Service implements the following design guidelines:
- rules allows to match by any criteria: header, content of the header, or route path.
1. Agnostic:
- Routing service is not aware of high-level concepts like organizations.
- The classification is done per-specification provided in a rules, to find the sharding key.
- The sharding key result is cached.
- The single sharding key cached is used to handle many similar requests.
- The classification is done per-specification provided in a rules, to find the classification key.
- The classification key result is cached.
- The single classification key cached is used to handle many similar requests.
The following diagram shows how a user request routes through DNS to the Routing Service deployed
as Cloudflare Worker and the router chooses a cell to send the request to.
@ -241,29 +241,10 @@ graph TD;
Each Cell will publish a precompiled list of routing rules that will be consumed by the Routing Service:
- The routing rules describe how to decode the request, find the sharding key, and make the routing decision.
- The routing rules are compiled during the deployment of the Routing Service.
- The deployment process fetches latest version of the routing rules from each Cell
that is part of Routing Service configuration.
- The compilation process merges the routing rules from all Cells.
- The conflicting rules prevent routing service from being compiled / started.
- Each routing rule entry has a unique identifier to ease the merge.
- The Routing Service would be re-deployed only if the list of rules was changed,
which shouldn't happen frequently, because we expect the majority of newly added endpoints
to already adhere to the prior route rules.
- The configuration describes from which Cells the routing rules need to be fetched during deploy.
- The published routing rules might make routing decision based on the secret. For example, if the session cookie
or authentication token has prefix `c100-` all requests are to be forwarded to the given Cell.
- The Cell does publish routing rules at `/api/v4/internal/cells/route_rules.json`.
- The rules published by Cell only include endpoints that the particular Cell can process.
- The Cell might request to perform dynamic classification based on sharding key, by configuring
routing rules to call `/api/v4/internal/cells/classify`.
- The routing rules should use `prefix` as a way to speed up classification. During the compilation phase
the routing service transforms all found prefixes into a decision tree to speed up any subsequent regex matches.
- Some of the prefixes need to be Cell independent, example Personal Access Tokens prefix need to be organization bound and not Cell bound.
We want the ability to move an organization from 1 cell to another without changing the Personal Access Token or any other token.
- The routing rules is ideally compiled into source code to avoid expensive parsing and evaluation of the rules
dynamically as part of deployment.
- The routing rules describe how to decode the request, find the classification key, and make the routing decision.
- The routing rules are static and defined ahead of time as part of HTTP Router deployment.
- The routing rules are defined as a JSON document describing in-order a sequence of operation.
- The routing rules might be compiled to application code to provide a way faster execution scheme.
The routing rules JSON structure describes all matchers:
@ -271,74 +252,65 @@ The routing rules JSON structure describes all matchers:
{
"rules": [
{
"id": "<unique-identifier>",
"cookies": {
"<cookie_name>": {
"prefix": "<match-given-prefix>",
"match_regex": "<regex_match>"
},
"<cookie_name2>": {
"prefix": "<match-given-prefix>",
"match_regex": "<regex_match>"
}
},
"headers": {
"<header_name>": {
"prefix": "<match-given-prefix>",
"match_regex": "<regex_match>"
},
"<header_name2>": {
"prefix": "<match-given-prefix>",
"match_regex": "<regex_match>"
},
},
"path": {
"prefix": "<match-given-prefix>",
"match_regex": "<regex_match>"
},
"method": ["<list_of_accepted_methods>"],
// If many rules are matched, define which one wins
"priority": 1000,
// Accept request and proxy to the Cell in question
"action": "proxy",
// Classify request based on regex matching groups
"action": "classify",
"classify": {
"keys": ["list_of_regex_match_capture_groups"]
"type": "session_prefix|project_path|...",
"value": "string_build_from_regex_matchers"
}
}
]
}
```
Example of the routing rules published by the Cell 100 that makes routing decision based session cookie, and secret.
The high priority is assigned since the routing rules is secret-based, and should take precedence before all other matchers:
Example of the routing rules that makes routing decision based session cookie, and secret:
```json
{
"rules": [
{
"id": "t4mkd5ndsk58si6uwwz7rdavil9m2hpq",
"cookies": {
"_gitlab_session": {
"prefix": "c100-" // accept `_gitlab_session` that are prefixed with `c100-`
"match_regex": "^(?<cell_name>cell.*:)" // accept `_gitlab_session` that are prefixed with `cell1:`
}
},
"action": "proxy",
"priority": 1000
"action": "classify",
"classify": {
"type": "session_prefix",
"value": "${cell_name}"
}
},
{
"id": "jcshae4d4dtykt8byd6zw1ecccl5dkts",
"headers": {
"GITLAB_TOKEN": {
"prefix": "C100_" // accept `GITLAB_TOKEN` that are prefixed with `C100_`
"match_regex": "^(?<cell_name>cell.*:)" // accept `_gitlab_session` that are prefixed with `cell1:`
}
},
"action": "proxy",
"priority": 1000
"action": "classify",
"classify": {
"type": "token_prefix",
"value": "${cell_name}"
}
}
]
}
@ -350,14 +322,13 @@ Example of the routing rules published by all Cells that makes routing decision
{
"rules": [
{
"id": "c9scvaiwj51a75kzoh917uwtnw8z4ebl",
"path": {
"prefix": "/api/v4/projects/", // speed-up rule matching
"match_regex": "^/api/v4/projects/(?<project_id_or_path_encoded>[^/]+)(/.*)?$"
},
"action": "classify",
"classify": {
"keys": ["project_id_or_path_encoded"]
"type": "project_id_or_path",
"value": "${project_id_or_path_encoded}"
}
}
]
@ -366,21 +337,16 @@ Example of the routing rules published by all Cells that makes routing decision
### Classification
Each Cell does implement classification endpoint:
The classification is implemented by [the Classify Service of the Topology Service](topology_service.md#classify-service).
- The classification endpoint is at `/api/v4/internal/cells/classify` (or gRPC endpoint).
- The classification endpoint accepts a list of the sharding keys. Sharding keys are decoded from request,
based on the routing rules provided by the Cell.
- The endpoint returns other equivalent sharding keys to pollute cache for similar requests.
- The classification endpoint uses REST (with mTLS) to secure access.
- The classification endpoint returns only cell name to which information should be routed.
- The classification could return other equivalent classification keys to pollute cache for similar requests.
This is to ensure that all similar requests can be handled quickly without having to classify each time.
- Routing Service tracks the health of Cells, and issues a `classify` request to Cells based on weights,
health of the Cell, or other defined criteria. Weights would indicate which Cell is preferred to perform the
classification of sharding keys.
- Routing Service retries the `classify` call for a reasonable amount of time.
The repetitive failure of Cell to `classify` is indicative of Cell being unhealthy.
- The `classify` result is cached regardless of returned `action` (proxy or reject).
- The HTTP Router retries the `classify` call for a reasonable amount of time.
- The classification for a given value is cached regardless of returned response (positive or negative).
The rejected classification is cached to prevent excessive amount of
requests for sharding keys that are not found.
requests for classification keys that are not found.
- The cached response is for time defined by `expiry` and `refresh`.
- The `expiry` defines when the item is removed from cache unless used.
- The `refresh` defines when the item needs to be reclassified if used.
@ -392,65 +358,46 @@ For the above example:
1. It selects the above `rule` for this request, which requests `classify` for `project_id_or_path_encoded`.
1. It decodes `project_id_or_path_encoded` to be `1000`.
1. Checks the cache if there's `project_id_or_path_encoded=1000` associated to any Cell.
1. Sends the request to `/api/v4/internal/cells/classify` if no Cells was found in cache.
1. Rails responds with the Cell holding the given project, and also all other equivalent sharding keys
1. Sends the request to `/api/v1/classify` (`type=project_id_or_path`, `value=1000`) if no Cells was found in cache.
1. Topology Service responds with the Cell holding the given project, and also all other equivalent classification keys
for the resource that should be put in the cache.
1. Routing Service caches for the duration specified in configuration, or response.
```json
# POST /api/v4/internal/cells/classify
# POST /api/v1/classify
## Request:
{
"metadata": {
"rule_id": "c9scvaiwj51a75kzoh917uwtnw8z4ebl",
"headers": {
"all_request_headers": "value"
},
"method": "GET",
"path": "/api/v4/projects/100/issues"
},
"keys": {
"project_id_or_path_encoded": 100
}
"type": "project_id_or_path",
"value": 1000
}
## Response:
{
"action": "proxy",
"proxy": {
"name": "cell_1",
"url": "https://cell1.gitlab.com"
"address": "cell1.gitlab.com"
},
"ttl": "10 minutes",
"matched_keys": [ // list of all equivalent keys that should be put in the cache
{ "project_id_or_path_encoded": 100 },
{ "project_id_or_path_encoded": "gitlab-org%2Fgitlab" },
{ "project_full_path": "gitlab-org/gitlab" },
{ "namespace_full_path": "gitlab-org" },
{ "namespace_id": 10 },
{ "organization_full_path": "gitlab-inc" },
{ "organization_id": 50 },
"cache": {
"refresh": "10 minutes",
"expiry": "10 minutes"
},
"other_classifications": [ // list of all equivalent keys that should be put in the cache
{ "type": "session_prefix", "value": "cell1" },
{ "type": "project_full_path", "value": "gitlab-org/gitlab" },
{ "type": "project_full_path", "value": "gitlab-org/gitlab" },
{ "type": "namespace_full_path", "value": "gitlab-org" }
]
}
```
The following code represents a negative response when a sharding key was not found:
The following code represents a negative response when a classification key was not found:
```json
# POST /api/v4/internal/cells/classify
## Request:
{
"metadata": {
"rule_id": "c9scvaiwj51a75kzoh917uwtnw8z4ebl",
"headers": {
"all_request_headers": "value"
},
"method": "GET",
"path": "/api/v4/projects/100/issues"
},
"keys": {
"project_id_or_path_encoded": 100
}
"type": "project_id_or_path",
"value": 1000
}
## Response:
@ -462,49 +409,16 @@ The following code represents a negative response when a sharding key was not fo
"cache": {
"refresh": "10 minutes",
"expiry": "10 minutes"
},
"matched_keys": [ // list of all equivalent keys that should be put in the cache
{ "project_id_or_path_encoded": 100 },
]
}
}
```
### Configuration
The Routing Service will use the configuration similar to this:
All configuration will be provided via environment variables:
```toml
[[cells]]
name=cell_1
url=https://cell1.gitlab.com
key=ABC123
classify_weight=100
[[cells]]
name=cell_2
url=https://cell2.gitlab.com
key=CDE123
classify_weight=1
[cache.memory.classify]
refresh_time=10 minutes
expiry_time=1 hour
[cache.external.classify]
refresh_time=30 minutes
expiry_time=6 hour
```
We assume that this is acceptable to provide a static list of Cells, because:
1. Static: Cells provisioned are unlikely to be dynamically provisioned and decommissioned.
1. Good enough: We can manage such list even up to 100 Cells.
1. Simple: We don't have to implement robust service discovery in the service,
and we have guarantee that this list is always exhaustive.
The configuration describes all Cells, URLs, zero-trust keys, and weights,
and how long requests should be cached. The `classify_weight` defines how often
the Cell should receive classification requests versus other Cells.
- HTTP Router will only configure an address to Topology Service
- The mTLS will be used when connecting to Topology Service to authentication / authorization.
### Deployment
@ -544,10 +458,10 @@ There are several phases to fully deploy the HTTP Routing service to GitLab.com.
1. `gitlab-org` is a top-level namespace and lives in `Cell US0` in the `GitLab.com Public` organization.
1. `my-company` is a top-level namespace and lives in `Cell EU0` in the `my-organization` organization.
### Router configured to perform static routing
### Router configured to perform the following routing
1. The Cell US0 supports all other public-facing projects.
1. The Cells is configured to generate all secrets and session cookies with a prefix like `eu0_` for Cell EU0.
1. The Cell EU0 configured to generate all secrets and session cookies with a prefix like `cell_eu0_`.
1. The Personal Access Token is scoped to Organization, and because the Organization is part only of a single Cell,
the PATs generated are prefixed with Cell identifier.
1. The Session Cookie encodes Organization in-use, and because the Organization is part only of a single Cell,
@ -555,53 +469,40 @@ There are several phases to fully deploy the HTTP Routing service to GitLab.com.
1. The Cell EU0 allows only private organizations, groups, and projects.
1. The Cell US0 is a target Cell for all requests unless explicitly prefixed.
Cell US0:
Router rules:
```json
{
"rules": [
{
"id": "tjh147se67wadjzum7onwqiad2b75uft",
"path": {
"prefix": "/"
},
"action": "proxy",
"priority": 1
}
]
}
```
Cell EU0:
```json
{
"rules": [
{
"id": "t4mkd5ndsk58si6uwwz7rdavil9m2hpq",
"cookies": {
"_gitlab_session": {
"prefix": "eu0_"
"regex_match": "^(?<cell_name>cell.*:)"
}
},
"path": {
"prefix": "/"
},
"action": "proxy",
"priority": 1000
"action": "classify",
"classify": {
"type": "session_prefix",
"value": "${cell_name}"
}
},
{
"id": "jcshae4d4dtykt8byd6zw1ecccl5dkts",
"headers": {
"GITLAB_TOKEN": {
"prefix": "eu0_"
"regex_match": "^(?<cell_name>cell.*-)"
}
},
"path": {
"prefix": "/"
},
"action": "proxy",
"priority": 1000
"action": "classify",
"classify": {
"type": "token_prefix",
"value": "${cell_name}"
}
},
{
"action": "classify",
"classify": {
"type": "first_cell",
}
}
]
}
@ -609,17 +510,24 @@ Cell EU0:
#### Goes to `/my-company/my-project` while logged in into Cell EU0
1. Because user switched the Organization to `my-company`, its session cookie is prefixed with `eu0_`.
1. User sends request `/my-company/my-project`, and because the cookie is prefixed with `eu0_` it is directed to Cell EU0.
1. Because user switched the Organization to `my-company`, its session cookie is prefixed with `cell_eu0_`.
1. User sends request `/my-company/my-project`, and because the cookie is prefixed with `cell_eu0_` it is directed to Cell EU0.
1. `Cell EU0` returns the correct response.
```mermaid
sequenceDiagram
participant user as User
participant router as Router
participant cache as Cache
participant ts as Topology Service
participant cell_eu0 as Cell EU0
participant cell_eu1 as Cell EU1
user->>router: GET /my-company/my-project<br/>_gitlab_session=eu0_uwwz7rdavil9
user->>router: GET /my-company/my-project<br/>_gitlab_session=cell_eu0_uwwz7rdavil9
router->>+cache: GetClassify(type=session_prefix, value=cell_eu0)
cache->>-router: NotFound
router->>+ts: Classify(type=session_prefix, value=cell_eu0)
ts->>-router: Proxy(address="cell-eu0.gitlab.com")
router->>cache: Cache(type=session_prefix, value=cell_eu0) = Proxy(address="cell-eu0.gitlab.com"))
router->>cell_eu0: GET /my-company/my-project
cell_eu0->>user: <h1>My Project...
```
@ -628,18 +536,24 @@ sequenceDiagram
1. User visits `/my-company/my-project`, and because it does not have session cookie, the request is forwarded to `Cell US0`.
1. User signs in.
1. GitLab sees that user default organization is `my-company`, so it assigns session cookie with `eu0_` to indicate that
1. GitLab sees that user default organization is `my-company`, so it assigns session cookie with `cell_eu0_` to indicate that
user is meant to interact with `my-company`.
1. User sends request to `/my-company/my-project` again, now with the session cookie that proxies to `Cell EU0`.
1. `Cell EU0` returns the correct response.
NOTE:
The `cache` is intentionally skipped here to reduce diagram complexity.
```mermaid
sequenceDiagram
participant user as User
participant router as Router
participant ts as Topology Service
participant cell_us0 as Cell US0
participant cell_eu0 as Cell EU0
user->>router: GET /my-company/my-project
router->>ts: Classify(type=first_cell)
ts->>router: Proxy(address="cell-us0.gitlab.com")
router->>cell_us0: GET /my-company/my-project
cell_us0->>user: HTTP 302 /users/sign_in?redirect=/my-company/my-project
user->>router: GET /users/sign_in?redirect=/my-company/my-project
@ -647,157 +561,39 @@ sequenceDiagram
cell_us0-->>user: <h1>Sign in...
user->>router: POST /users/sign_in?redirect=/my-company/my-project
router->>cell_us0: POST /users/sign_in?redirect=/my-company/my-project
cell_us0->>user: HTTP 302 /my-company/my-project<br/>_gitlab_session=eu0_uwwz7rdavil9
user->>router: GET /my-company/my-project<br/>_gitlab_session=eu0_uwwz7rdavil9
router->>cell_eu0: GET /my-company/my-project<br/>_gitlab_session=eu0_uwwz7rdavil9
cell_us0->>user: HTTP 302 /my-company/my-project<br/>_gitlab_session=cell_eu0_uwwz7rdavil9
user->>router: GET /my-company/my-project<br/>_gitlab_session=cell_eu0_uwwz7rdavil9
router->>ts: Classify(type=session_prefix, value=cell_eu0)
ts->>router: Proxy(address="cell-eu0.gitlab.com")
router->>cell_eu0: GET /my-company/my-project<br/>_gitlab_session=cell_eu0_uwwz7rdavil9
cell_eu0->>user: <h1>My Project...
```
#### Goes to `/gitlab-org/gitlab` after last step
User visits `/my-company/my-project`, and because it does not have a session cookie, the request is forwarded to `Cell US0`.
```mermaid
sequenceDiagram
participant user as User
participant router as Router
participant cell_eu0 as Cell EU0
participant cell_us0 as Cell US0
user->>router: GET /gitlab-org/gitlab<br/>_gitlab_session=eu0_uwwz7rdavil9
router->>cell_eu0: GET /gitlab-org/gitlab
cell_eu0->>user: HTTP 404
```
### Router configured to perform dynamic routing based on classification
The Cells publish route rules that allows to classify the requests.
Cell US0 and EU0:
```json
{
"rules": [
{
"id": "tjh147se67wadjzum7onwqiad2b75uft",
"path": {
"prefix": "/",
"regex": "^/(?top_level_group)[^/]+(/.*)?$",
},
"action": "classify",
"classify": {
"keys": ["top_level_group"]
}
},
{
"id": "jcshae4d4dtykt8byd6zw1ecccl5dkts",
"path": {
"prefix": "/"
},
"action": "proxy"
}
]
}
```
#### Goes to `/my-company/my-project` while logged in into Cell EU0
1. The `/my-company/my-project/` is visited.
1. Router decodes sharding key `top_level_group=my-company`.
1. Router checks if this sharding key is cached.
1. Because it is not, the classification request is sent to a random Cell to `/classify`.
1. The response of classify is cached.
1. The request is then proxied to Cell returned by classification.
User visits `/gitlab-org/gitlab`, and because it does have a session cookie, the request is forwarded to `Cell EU0`.
There is no need to ask Topology Service, since the session cookie is cached.
```mermaid
sequenceDiagram
participant user as User
participant router as Router
participant cache as Cache
participant cell_us0 as Cell US0
participant ts as Topology Service
participant cell_eu0 as Cell EU0
user->>router: GET /my-company/my-project
router->>cache: CACHE_GET: top_level_group=my-company
cache->>router: CACHE_NOT_FOUND
router->>cell_us0: POST /api/v4/internal/cells/classify<br/>top_level_group=my-company
cell_us0->>router: CLASSIFY: top_level_group=my-company, cell=cell_eu0
router->>cache: CACHE_SET: top_level_group=my-company, cell=cell_eu0
participant cell_eu1 as Cell EU1
user->>router: GET /my-company/my-project<br/>_gitlab_session=cell_eu0_uwwz7rdavil9
router->>+cache: GetClassify(type=session_prefix, value=cell_eu0)
cache->>-router: Proxy(address="cell-eu0.gitlab.com"))
router->>cell_eu0: GET /my-company/my-project
cell_eu0->>user: <h1>My Project...
```
### Goes to `/my-company/my-project` while not logged in
1. The `/my-company/my-project/` is visited.
1. Router decodes sharding key `top_level_group=my-company`.
1. Router checks if this sharding key is cached.
1. Because it is not, the classification request is sent to a random Cell to `/classify`.
1. The response of `classify` is cached.
1. The request is then proxied to Cell returned by classification.
1. Because project is private, user is redirected to sign in.
1. The sign-in since is defined to be handled by all Cells, so it is proxied to a random Cell.
1. User visits the `/my-company/my-project/` again after logging in.
1. The `top_level_group=my-company` is proxied to the correct Cell.
```mermaid
sequenceDiagram
participant user as User
participant router as Router
participant cache as Cache
participant cell_us0 as Cell US0
participant cell_eu0 as Cell EU0
user->>router: GET /my-company/my-project
router->>cache: CACHE_GET: top_level_group=my-company
cache->>router: CACHE_NOT_FOUND
router->>cell_us0: POST /api/v4/internal/cells/classify<br/>top_level_group=my-company
cell_us0->>router: CLASSIFY: top_level_group=my-company, cell=cell_eu0
router->>cache: CACHE_SET: top_level_group=my-company, cell=cell_eu0
router->>cell_eu0: GET /my-company/my-project
cell_eu0->>user: HTTP 302 /users/sign_in?redirect=/my-company/my-project
user->>router: GET /users/sign_in?redirect=/my-company/my-project
router->>cell_us0: GET /users/sign_in?redirect=/my-company/my-project
cell_us0-->>user: <h1>Sign in...
user->>router: POST /users/sign_in?redirect=/my-company/my-project
router->>cell_eu0: POST /users/sign_in?redirect=/my-company/my-project
cell_eu0->>user: HTTP 302 /my-company/my-project
user->>router: GET /my-company/my-project
router->>cache: CACHE_GET: top_level_group=my-company
cache->>router: CACHE_FOUND: cell=cell_eu0
router->>cell_eu0: GET /my-company/my-project
cell_eu0->>user: <h1>My Project...
```
#### Goes to `/gitlab-org/gitlab` after last step
1. Because the `/gitlab-org` is not found in cache, it will be classified and then directed to correct Cell.
```mermaid
sequenceDiagram
participant user as User
participant router as Router
participant cache as Cache
participant cell_us0 as Cell US0
participant cell_eu0 as Cell EU0
user->>router: GET /gitlab-org/gitlab
router->>cache: CACHE_GET: top_level_group=gitlab-org
cache->>router: CACHE_NOT_FOUND
router->>cell_us0: POST /api/v4/internal/cells/classify<br/>top_level_group=gitlab-org
cell_us0->>router: CLASSIFY: top_level_group=gitlab-org, cell=cell_us0
router->>cache: CACHE_SET: top_level_group=gitlab-org, cell=cell_us0
router->>cell_us0: GET /gitlab-org/gitlab
cell_us0->>user: <h1>My Project...
```
### Performance and reliability considerations
- It is expected that each Cell can classify all sharding keys.
- Alternatively the classification could be done by Cluster-wide Data Provider
if it would own all data required to classify.
- The published routing rules allow to define static criteria, allowing to make routing decision
only on a secret. As a result, the Routing Service doesn't add any latency
for request processing, and superior resiliency.
- It is expected that there will be penalty when learning new sharding key. However,
- It is expected that there will be penalty when learning new classification key. However,
it is expected that multi-layer cache should provide a very high cache-hit-ratio,
due to low cardinality of sharding key. The sharding key would effectively be mapped
due to low cardinality of classification key. The classification key would effectively be mapped
into resource (organization, group, or project), and there's a finite amount of those.
## Alternatives
@ -809,7 +605,7 @@ describes an approach where Cell answers with `X-Gitlab-Cell-Redirect` to redire
- This is based on a need to buffer the whole request (headers + body) which is very memory intensive.
- This proposal does not provide an easy way to handle mixed deployment of Cells, where Cells might be running different versions.
- This proposal likely requires caching significantly more information, since it is based on requests, rather than on decoded sharding keys.
- This proposal likely requires caching significantly more information, since it is based on requests, rather than on decoded classification keys.
### Learn request
@ -819,7 +615,7 @@ is done in a single go in a form of pre-flight check `/api/v4/internal/cells/lea
- This makes the whole routes learning dynamic, and dependent on availability of the Cells.
- This proposal does not provide an easy way to handle mixed deployment of Cells, where Cells might be running different versions.
- This proposal likely requires caching significantly more information, since it is based on requests, rather than on decoded sharding keys.
- This proposal likely requires caching significantly more information, since it is based on requests, rather than on decoded classification keys.
## FAQ

View File

@ -36,7 +36,7 @@ From a development and infrastructure perspective we want to achieve the followi
1. All Cells are accessible under a single domain.
1. Cells are mostly independent with minimal data sharing. All stateful data is segregated, and minimal data sharing is needed initially. This includes any database and cloud storage buckets.
1. Cells need to be able to run independently with different versions.
1. An architecture that allows for eventual cluster-wide data sharing.
1. A cluster-wide service is provided to synchronize state between all Cells.
1. A routing solution that is robust, but simple.
1. All identifiers (primary keys, user, group, and project names) are unique across the cluster, so that we can perform logical re-balancing at a later time. This includes all database tables, except ones using schemas `gitlab_internal`, or `gitlab_shared`.
1. Because all users and groups are unique across the cluster, the same user can access other Organizations and groups at GitLab.com in [Cells 2.0](cells-2.0.md).
@ -55,9 +55,8 @@ The following statements describe a high-level proposal to achieve a Cells 1.0:
1. Terms used:
1. Primary Cell: The current GitLab.com deployment. A special purpose Cell that serves
as a cluster-wide service in this architecture.
1. Secondary Cells: A Cell that connects to the Primary Cell to ensure cluster-wide uniqueness.
1. Cell: A single isolated deployment of GitLab that connects to the Topology Service.
1. Topology Service: The central service that is the authoritative entity in a cluster. Provides uniqueness and routing information.
1. Organization properties:
@ -80,30 +79,30 @@ The following statements describe a low-level development proposal to achieve th
1. Each secret token (personal access token, build token, runner token, etc.) generated by the application includes a unique identifier indicating the Cell, for example `us0`. The identifier should try to obfuscate information about the Cell.
1. The session cookie sent to the client is prefixed with a unique identifier indicating the Cell, for example `us0`.
1. The application configuration includes a Cell secret prefix, and information about the Primary Cell.
1. The application configuration includes a Cell secret prefix, and the location of the Topology Service.
1. User always logs into the Cell on which the user was created.
1. Database properties:
1. Each primary key in the database is unique across the cluster. We use database sequences that are allocated from the Primary Cell.
1. Each primary key in the database is unique across the cluster. We use database sequences that are allocated by the Topology Service.
1. We require each table to be classified: to be cluster-wide or Cell-local.
1. We follow a model of eventual consistency:
1. All cluster-wide tables are stored in a Cell-local database.
1. All cluster-wide tables retain unique constraints across the whole cluster.
1. Locally stored cluster-wide tables contain information required by this Cell only, unless it is the Primary Cell.
1. Locally stored cluster-wide tables contain information required by this Cell only.
1. The cluster-wide tables are restricted to be modified by the Cell that is authoritative over the particular record:
1. The user record can be modified by the given Cell only if that Cell is the authoritative source of this record.
1. In Cells 1.0 we are likely to not be replicating data across cluster,
so the authoritative source is the Cell that contains the record.
1. The Primary Cell serves as a single source of truth for the uniqueness constraint (be it ID or user, group, project uniqueness).
1. Secondary Cells use APIs to claim usernames, groups or projects.
1. The Primary Cell holds information about all usernames, groups and projects, and the Cell holding the record.
1. The Primary Cell does not hold source information (actual user or project records), only references that are indicative of the Cell where that information is stored.
1. The Topology Service serves as a single source of truth for the uniqueness constraint (be it ID or user, group, project uniqueness).
1. All Cells use gRPC to claim usernames, groups or projects.
1. The Topology Service holds metadata information that allows to know on which Cell the username, group or project is.
1. The Topology Service does not hold source information (actual user or project records), only references that are indicative of the Cell where that information is stored.
1. Routing properties:
1. We implement a static routing service that performs secret-based routing based on the prefix.
1. The routing service is implemented as a Cloudflare Worker and is run on edge. The routing service is run with a static list of Cells. Each Cell is described by a proxy URL, and a prefix.
1. We implement a routing service that performs secret-based routing based on the prefix.
1. The routing service is implemented as a Cloudflare Worker and is run on edge. The routing service defines a set of rules, and uses Topology Service to classify how to route data.
1. Cells are exposed over the public internet, but might be guarded with Zero Trust.
### Architecture overview
@ -116,109 +115,56 @@ cloud "Cloudflare" as CF {
}
node "GitLab Inc. Infrastructure" {
node "Primary Cell" as PC {
frame "GitLab Rails" as PC_Rails {
[Puma + Workhorse + LB] as PC_Puma
[Sidekiq] as PC_Sidekiq
}
node "Cell Services" as Cell_Services {
[Topology Service] as TS
[Cloud Spanner] as TS_CS
[Container Registry] as PC_Registry
database DB as PC_DB {
frame "PostgreSQL Cluster" as PC_PSQL {
package "ci" as PC_PSQL_ci {
[gitlab_ci] as PC_PSQL_gitlab_ci
}
package "main" as PC_PSQL_main {
[gitlab_main_clusterwide] as PC_PSQL_gitlab_main_clusterwide
[gitlab_main_cell] as PC_PSQL_gitlab_main_cell
}
PC_PSQL_main -[hidden]-> PC_PSQL_ci
}
frame "Redis Cluster" as PC_Redis {
[Redis (many)] as PC_Redis_many
}
frame "Gitaly Cluster" as PC_Gitaly {
[Gitaly Nodes (many)] as PC_Gitaly_many
}
}
PC_Rails -[hidden]-> PC_DB
TS --> TS_CS
}
node "Secondary Cell" as SC {
frame "GitLab Rails" as SC_Rails {
[Puma + Workhorse + LB] as SC_Puma
[Sidekiq] as SC_Sidekiq
node "A Cell" as Cell {
frame "GitLab Rails" as Cell_Rails {
[Puma + Workhorse + LB] as Cell_Puma
[Sidekiq] as Cell_Sidekiq
}
[Container Registry] as SC_Registry
[Container Registry] as Cell_Registry
database DB as SC_DB {
frame "PostgreSQL Cluster" as SC_PSQL {
package "ci" as SC_PSQL_ci {
[gitlab_ci] as SC_PSQL_gitlab_ci
database DB as Cell_DB {
frame "PostgreSQL Cluster" as Cell_PSQL {
package "ci" as Cell_PSQL_ci {
[gitlab_ci] as Cell_PSQL_gitlab_ci
}
package "main" as SC_PSQL_main {
[gitlab_main_clusterwide] as SC_PSQL_gitlab_main_clusterwide
[gitlab_main_cell] as SC_PSQL_gitlab_main_cell
package "main" as Cell_PSQL_main {
[gitlab_main_clusterwide] as Cell_PSQL_gitlab_main_clusterwide
[gitlab_main_cell] as Cell_PSQL_gitlab_main_cell
}
SC_PSQL_main -[hidden]-> SC_PSQL_ci
PC_PSQL_main -[hidden]-> Cell_PSQL_ci
}
frame "Redis Cluster" as SC_Redis {
[Redis (many)] as SC_Redis_many
frame "Redis Cluster" as Cell_Redis {
[Redis (many)] as Cell_Redis_many
}
frame "Gitaly Cluster" as SC_Gitaly {
[Gitaly Nodes (many)] as SC_Gitaly_many
frame "Gitaly Cluster" as Cell_Gitaly {
[Gitaly Nodes (many)] as Cell_Gitaly_many
}
}
SC_Rails -[hidden]-> SC_DB
Cell_Rails -[hidden]-> Cell_DB
}
}
CF_RSW --> PC_Puma
CF_RSW --> PC_Registry
CF_RSW --> SC_Puma
CF_RSW --> SC_Registry
@enduml
```
### API overview
```plantuml
@startuml
skinparam FrameBackgroundcolor white
node "GitLab Inc. Infrastructure" {
node "Primary Cell" as PC {
frame "GitLab Rails" as PC_Rails {
[Puma + Workhorse + LB] as PC_Puma
[Sidekiq] as PC_Sidekiq
}
}
node "Secondary Cell" as SC {
frame SC_Rails [
{{
[Puma + Workhorse + LB] as SC_Puma
[Sidekiq] as SC_Sidekiq
}}
]
}
}
SC_Rails -up-> PC_Puma : "Primary Cell API:\n/api/v4/internal/cells/..."
CF_RSW --> Cell_Puma
CF_RSW --> Cell_Registry
CF_RSW --> Cell_Puma
CF_RSW --> Cell_Registry
CF_RSW --> TS
Cell_Puma --> TS
Cell_Sidekiq --> TS
@enduml
```
@ -238,196 +184,27 @@ The GitLab configuration in `gitlab.yml` is extended with the following paramete
```yaml
production:
gitlab:
primary_cell:
url: https://cell1.gitlab.com
token: abcdef
topology_service:
address: https://cell1.gitlab.com
certificate: ...
secrets_prefix: kPptz
```
1. `primary_cell:` configured on Secondary Cells, and indicates the URL endpoint to access the Primary Cell API.
1. `secrets_prefix:` can be used on all Cells, and indicates that each secret and session cookie is prefixed with this identifier.
### Topology Service
### Primary Cell
The Primary Cell serves a special purpose to ensure cluster uniqueness.
The Primary Cell exposes a set of API interfaces to be used by Secondary Cells.
The API is considered internal, and is guarded with a secret that is shared with Secondary Cells.
1. `POST /api/v4/internal/cells/database/claim`
1. Request:
- `table`: table name for the allocated sequence, for example `projects`
- `count`: number of IDs to claim that are guaranteed to be unique, for example 100_000
1. Response:
- `start`: the start sequence value
- `limit`: the allowed maximum sequence value
1. `POST /api/v4/internal/cells/routes`
1. Request:
- `path`: the full path to a resource, for example `my-company/my-project`
- `source_type`: the class name for the container holding the resource, for example `project`
- `source_id`: the source ID for the container holding the resource, for example `1000`
- `namespace_id`: the underlying namespace associated with the resource, for example `32`
- `name`: the display name for the resource
- `cell_id`: the identifier of the Cell holding the resource
1. Response:
- 201: Created: The resource was created.
- `id:`: The ID of the resource as stored on the Primary Cell.
- 409: Conflict: The resource already exists.
1. Behavior:
1. The endpoint returns an `id`. The same ID has to be used to insert a record in `routes` table for the calling cell.
1. `GET /api/v4/internal/cells/routes/:id`
1. Request:
- `id`: resource identifier
1. Response:
- 200: OK: The resource was found. For response parameters look at `POST /api/v4/internal/cells/routes` request parameters.
- 404: Not found: The resource does not exit.
1. `PUT /api/v4/internal/cells/routes/:id`
1. Request: For parameters look at `POST /api/v4/internal/cells/routes` request parameters.
1. Response:
- 200: OK: The resource was updated.
- 403: Forbidden: The resource cannot be modified, because it is owned by another `cell_id`.
- 404: Not found: The resource does not exit.
- 409: Conflict: The resource already exists. The uniqueness constraint on `path` failed.
1. Behavior:
1. The endpoint modifies the given resource as long the `cell_id` matches.
1. `DELETE /api/v4/internal/cells/routes/:id`
1. Request: For parameters look at `POST /api/v4/internal/cells/routes` request parameters.
1. Response:
- 200: OK: The resource with given parameters was successfully deleted.
- 404: Not found: The given resource was not found.
- 400: Bad request: The resource failed to be deleted.
1. `POST /api/v4/internal/cells/redirect_routes`
1. `GET /api/v4/internal/cells/redirect_routes/:id`
1. `PUT /api/v4/internal/cells/redirect_routes/:id`
1. `DELETE /api/v4/internal/cells/redirect_routes/:id`
### Secondary Cell
The Secondary Cell does not expose any specific API at this point.
The Secondary Cell implements a solution to guarantee uniqueness of primary database keys.
1. `ReplenishDatabaseSequencesWorker`: this worker runs periodically, check all sequences, and replenish them.
#### Simple uniqueness of Database Sequences
Simple uniqueness of database sequences refers to
the practice of claiming, using and replenishing a cluster-wide unique range of IDs for a table.
Our DDL schema uses ID generation in the form: `id bigint DEFAULT nextval('product_analytics_events_experimental_id_seq'::regclass) NOT NULL`.
The `/api/v4/internal/cells/database/claim` would execute the following logic
to atomically claim a range of IDs.
```ruby
def claim_table_seq(table, count)
sql = <<-SQL
BEGIN;
-- `ALTER SEQUENCE` effectively locks `some_seq` for write and read.
ALTER SEQUENCE seq_name INCREMENT BY 1000;
SELECT nextval(seq_name); -- Suppose this returned 1001.
ALTER SEQUENCE seq_name INCREMENT BY 1; -- UNDO "INCREMENT BY 1000".
INSERT INTO cells_sequence_claims (cell_id, table_name, start_id, end_id)
VALUES (cell_id, table_name, 2, 1001);
COMMIT;
SQL
seq_name = "#{table}_id_seq"
last_id = select_one(sql, seq_name, limit, seq_name, seq_name).first
{ start: last_id - count, limit: count }
end
```
#### Replenishing available IDs
The `ReplenishDatabaseSequencesWorker` would check how much space is left in a sequence, and request a new range if the value goes below the threshold.
```ruby
def replenish_table_seq(table, lower_limit, count)
seq_name = "#{table}_id_seq"
# maxval is not existing, so it would have to be implemented different way, but this is to showcase purposes
last_id, max_id = select_one("SELECT currval(?), maxval(?)", seq_name, seq_name)
return if max_id - last_id > lower_limit
new_start, new_limit = post("/api/v4/internal/cells/database/claim", { table: table, count: count })
execute("ALTER SEQUENCE RESTART ? MAXVALUE ?", new_start, new_limit)
end
```
This makes us lose the `lower_limit` of IDs of sequence creating gaps. However, in this model we need to replenish the
sequence ahead of time, otherwise we will have catastrophic failure on inserting new records.
The above claiming and replenishing approach can potentially waste too much ID space
claimed by each table.
For example, after having claimed the range from `101` to `200`,
a table might choose to replenish after it's used up the first 80 IDs (`101` to `180`)
leaving the remaining 20 IDs to be wasted.
To efficiently utilize all claimed IDs, we introduce the concept of robust uniqueness
in which a table maintains two alternating sequences and uses a custom implementation for `nextval` and similar functions.
#### Robust uniqueness of table sequences
This is very similar to simple uniqueness, with these additions:
- We use and alternate between two sequences per-table (A/B), fully utilizing allocated sequences.
- We change the `DEFAULT` to accept two arguments to function `nextval2(table_id_seq_a::regclass, table_id_seq_b::regclass)`.
- We replenish the sequence as soon as it runs out of free IDs.
- We don't create sequence gaps.
```sql
CREATE FUNCTION nextval2(seq_a oid, seq_b oid) RETURNS bigint
LANGUAGE plpgsql
AS $$
BEGIN
-- pick from sequence that has lower number
-- as we want to retain monotonically increasing numbers
-- when allocation fails (as the sequence is exhausted) switch to another one
SELECT last_value INTO seq_a_last_value FROM seq_a;
SELECT last_value INTO seq_b_last_value FROM seq_b;
IF seq_a_last_value < seq_b_last_value
BEGIN TRY
RETURN nextval(seq_a);
END TRY
BEGIN CATCH
RETURN nextval(seq_b);
END CATCH;
ELSE
BEGIN TRY
RETURN nextval(seq_b);
END TRY
BEGIN CATCH
RETURN nextval(seq_a);
END CATCH;
END
END
$$;
```
All services supported are described in dedicated documented about [Topology Service](../topology_service.md).
## Pros
- The proposal is lean:
- Each Cell holds only a fraction of the data that is required for the cluster to operate.
- The tables marked as `main_clusterwide` that are stored locally can be selectively replicated across Cells following a mesh-architecture.
- Based on the assumption that Cells require only a fraction of shared data (like users), it is expected that Secondary Cells might need a small percentage of records across the whole cluster.
- The primary Cell is a single point of failure only for a limited set of features:
- Uniqueness is enforced by the primary Cell.
- The temporary reliability of the primary Cell has a limited impact on Secondary Cells.
- Secondary Cells would not be able to enforce unique constraints: create group, project, or user.
- Other functionality of Secondary Cells would continue working as is: push, run CI.
- Based on the assumption that Cells require only a fraction of shared data (like users), it is expected that Cells might need a small percentage of records across the whole cluster.
- The Topology Service is a single point of failure:
- Reduced set of features allows to make it highly-available service.
- Use highly-available database solution (Cloud Spanner).
- The routing layer makes this service very simple, because it is secret-based and uses prefix.
- Reliability of the service is not dependent on Cell availability, because at this stage no dynamic classification is required.
- We anticipate that the routing layer will evolve to perform regular classification at a later point.
- Reliability of the service is not dependent on Cell availability. It depends on availability of Topology Service to perform classification.
- Mixed-deployment compatible by design.
- We do not share database connections. We expose APIs to interact with cluster-wide data.
- The application is responsible to support API compatibility across versions, allowing us to easily support many versions of the application running from day zero.
@ -465,14 +242,14 @@ The table below is a comparison between the existing GitLab.com features, and no
## Questions
1. How do we create new Organizations with the user on Secondary Cell?
1. How do we create new Organizations with the user on additional Cells?
To be defined.
1. How do we register new users for the existing Organization on Secondary Cell?
1. How do we register new users for the existing Organization on additional Cell?
If an Organization is already created, users can be invited.
We can then serve the registration flow from Secondary Cell.
We can then serve the registration flow from additional Cell.
1. How would users log in?
@ -480,9 +257,9 @@ The table below is a comparison between the existing GitLab.com features, and no
- SAML: `https://<GITLAB_DOMAIN>/users/auth/saml/callback` would receive `?organization=gitlab-inc` which would be routed to the correct Cell.
- This would require using the dynamic routing method with a list of Organizations available using a solution with high availability.
1. How do we add a new table if it is initially deployed on Secondary Cell?
1. How do we add a new table if it is initially deployed on additional Cell?
The Primary Cell is ensuring uniqueness of sequences, so it needs to have `sequence`.
The Topology Service is ensuring uniqueness of sequences, so it needs to have `sequence`.
1. Is Container Registry cluster-wide or cell-local?
@ -498,8 +275,8 @@ The table below is a comparison between the existing GitLab.com features, and no
- GitLab Pages need to be run as a single service that is not run as part of a Cell.
- Because GitLab Pages use the API we need to make them routable.
- Similar to `routes`, claim `pages_domain` on the Primary Cell
- Implement dynamic classification in the routing service, based on a sharding key.
- Similar to `routes`, claim `pages_domain` on the Topology Service
- Implement dynamic classification in the routing service, based on a classification key.
- Cons: This adds another table that has to be kept unique cluster-wide.
Alternatively:
@ -547,22 +324,14 @@ The table below is a comparison between the existing GitLab.com features, and no
Since we don't yet synchronize across the cluster, admin accounts would have to be provided per Cell.
This might be solved by GitLab Dedicated already?
1. Is it a Primary Cell or cluster-wide service?
The Primary Cell in fact serves as a cluster-wide service. Depending on our intent it could be named the following:
- Primary Cell: To clearly state that the Primary Cell has a special purpose today, but we rename it later.
- Cluster-wide Data Provider
- Topology Service: Alternative name to Cluster-wide Data Provider, indicating that the Primary Cell would implement a Topology Service today.
1. How are secrets are generated?
The Cell prefix is used to generate a secret in a way that encodes the prefix. The prefix is added to the generated secret.
Example:
- GitLab Runner Tokens are generated in the form: `glrt-2CR8_eVxiioB1QmzPZwa`
- GitLab Runner Tokens are generated in the form: `glrt-2CR8_XYZ`
- For Cell prefix: `secrets_prefix: kPptz`
- We would generate Runner tokens in the form: `glrt-kPptz_2CR8_eVxiioB1QmzPZwa`
- We would generate Runner tokens in the form: `glrt-kPptz_2CR8_XYZ`
1. Why secrets-based routing instead of path-based routing?
@ -578,7 +347,7 @@ The table below is a comparison between the existing GitLab.com features, and no
how many routes are already classified can be found [in this comment](https://gitlab.com/gitlab-org/gitlab/-/issues/430330#note_1633125914).
- In each case the routing service needs to be able to dynamically classify existing routes
based on some defined criteria, requiring significant development effort, and increasing the
dependency on the Primary Cell or Cluster-wide service.
dependency on the Topology Service.
By following secret-based routing we can cut a lot of initial complexity, which allows us to
make the best decision at a later point:
@ -619,21 +388,18 @@ The table below is a comparison between the existing GitLab.com features, and no
1. How would the Cell find users or projects?
To be defined. However, we would need `Primary Cell`/`Cluster-wide Data Provider` access to the `routes` table that contains a mapping of all names into objects (group, project, user) and the Cell holding the information.
The Cell would use [Classify Service](../topology_service.md#classify-service) of Topology Service.
1. Would the User Profile be public if created for enterprise customer?
No. Users created on another Cell in a given Organization would be limited to this Organization only.
The User Profile would be available as long as you are logged in.
1. What is the resiliency of a Primary Cell exposing cluster-wide API?
1. What is the resiliency of a Topology Service exposing cluster-wide API?
The API would be responsible for ensuring uniqueness of: User, Groups, Projects, Organizations, SSH keys, Pages domains, e-mails.
The API would also be responsible for classifying a sharding key for the routing service.
We need to ensure that the Primary Cell cluster-wide API is highly available. The solution here could be to:
1. Run the Primary Cell API as an additional node that has dedicated database replica and can work while the main Cell is down.
1. Implement a Primary Cell API on top of another highly available database in a different technology than Rails. Forward the API write calls to this storage (claim), but make read API calls (classify) to use this storage.
The API would also be responsible for classifying a classification key for the routing service.
We need to ensure that the Topology Service cluster-wide API is highly available.
1. How can instance-wide CI runners be configured on the new cells?

View File

@ -116,6 +116,19 @@ graph TD;
end
```
### Configuration
The Topology Service will use `config.toml` to configure all service parameters.
#### List of Cells
```toml
[[cells]]
id = 1
address = "cell-us-1.gitlab.com"
session_prefix = "cell1:"
```
### Sequence Service
```proto
@ -216,6 +229,7 @@ endpoints for Claims within a transaction.
enum ClassifyType {
Route = 1;
Login = 2;
SessionPrefix = 3;
}
message ClassifyRequest {
@ -280,6 +294,27 @@ sequenceDiagram
The sign-in request going to Cell 1 might at some point later be round-rubin routed to all Cells,
as each Cell should be able to classify user and redirect it to correct Cell.
#### Session cookie classification workflow with Classify Service
```mermaid
sequenceDiagram
participant User1
participant HTTP Router
participant TS / Classify Service
participant Cell 1
participant Cell 2
User1->> HTTP Router :GET "/gitlab-org/gitlab/-/issues"<br>Cookie: _gitlab_session=cell1:df1f861a9e609
Note over HTTP Router: Extract "cell1" from `_gitlab_session`
HTTP Router->> TS / Classify Service: Classify(SessionPrefix) "cell1"
TS / Classify Service->>HTTP Router: gitlab-org/gitlab => Cell 1
HTTP Router->> Cell 1: GET "/gitlab-org/gitlab/-/issues"<br>Cookie: _gitlab_session=cell1:df1f861a9e609
Cell 2->> HTTP Router: Issues Page Response
HTTP Router->>User1: Issues Page Response
```
The session cookie will be validated with `session_prefix` value.
### Metadata Service (**future**, implemented for Cells 1.5)
The Metadata Service is a way for Cells to distribute information cluster-wide:
@ -383,8 +418,6 @@ sequenceDiagram
## Reasons
The original [Cells 1.0](iterations/cells-1.0.md) described [Primary Cell API](iterations/cells-1.0.md#primary-cell), this changes this decision to implement Topology Service for the following reasons:
1. Provide stable and well described set of cluster-wide services that can be used
by various services (HTTP Routing Service, SSH Routing Service, each Cell).
1. As part of Cells 1.0 PoC we discovered that we need to provide robust classification API

View File

@ -21,8 +21,8 @@ its framework is removed.
- To create, edit, and delete compliance frameworks, users must have either:
- The Owner or Maintainer role in the top-level group.
- Be assigned a [custom role](../../user/custom_roles/abilities.md) with the `admin_compliance_framework`
[custom permission](../../user/custom_roles/abilities.md#compliance-management).
- Be assigned a [custom role](../custom_roles.md) with the `admin_compliance_framework`
[custom permission](../custom_roles/abilities.md#compliance-management).
- To add or remove a compliance framework to or from a project, the group to which the project belongs must have a
compliance framework.
@ -30,15 +30,15 @@ its framework is removed.
You can create, edit, or delete a compliance framework from a compliance framework report. For more information, see:
- [Create a new compliance framework](../../user/compliance/compliance_center/compliance_frameworks_report.md#create-a-new-compliance-framework).
- [Edit a compliance framework](../../user/compliance/compliance_center/compliance_frameworks_report.md#edit-a-compliance-framework).
- [Delete a compliance framework](../../user/compliance/compliance_center/compliance_frameworks_report.md#delete-a-compliance-framework).
- [Create a new compliance framework](../compliance/compliance_center/compliance_frameworks_report.md#create-a-new-compliance-framework).
- [Edit a compliance framework](../compliance/compliance_center/compliance_frameworks_report.md#edit-a-compliance-framework).
- [Delete a compliance framework](../compliance/compliance_center/compliance_frameworks_report.md#delete-a-compliance-framework).
You can create, edit, or delete a compliance framework from a compliance projects report. For more information, see:
- [Create a new compliance framework](../../user/compliance/compliance_center/compliance_projects_report.md#create-a-new-compliance-framework).
- [Edit a compliance framework](../../user/compliance/compliance_center/compliance_projects_report.md#edit-a-compliance-framework).
- [Delete a compliance framework](../../user/compliance/compliance_center/compliance_projects_report.md#delete-a-compliance-framework).
- [Create a new compliance framework](../compliance/compliance_center/compliance_projects_report.md#create-a-new-compliance-framework).
- [Edit a compliance framework](../compliance/compliance_center/compliance_projects_report.md#edit-a-compliance-framework).
- [Delete a compliance framework](../compliance/compliance_center/compliance_projects_report.md#delete-a-compliance-framework).
Subgroups and projects have access to all compliance frameworks created on their top-level group. However, compliance frameworks cannot be created, edited,
or deleted at the subgroup or project level. Project owners can choose a framework to apply to their projects.
@ -48,7 +48,7 @@ or deleted at the subgroup or project level. Project owners can choose a framewo
Add a compliance framework to a project. Compliance frameworks cannot be added to projects in personal namespaces.
To assign a compliance framework to a project, apply the compliance framework through the
[Compliance projects report](../../user/compliance/compliance_center/compliance_projects_report.md#apply-a-compliance-framework-to-projects-in-a-group).
[Compliance projects report](../compliance/compliance_center/compliance_projects_report.md#apply-a-compliance-framework-to-projects-in-a-group).
You can use the [GraphQL API](../../api/graphql/reference/index.md#mutationprojectsetcomplianceframework) to add a
compliance framework to a project.
@ -68,7 +68,7 @@ A compliance framework that is set to default has a **default** label.
### Set and remove a default by using the compliance center
To set as default (or remove the default) from [compliance projects report](../../user/compliance/compliance_center/compliance_projects_report.md#compliance-projects-report):
To set as default (or remove the default) from [compliance projects report](../compliance/compliance_center/compliance_projects_report.md#compliance-projects-report):
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Secure > Compliance center**.
@ -77,7 +77,7 @@ To set as default (or remove the default) from [compliance projects report](../.
1. Select **Set as default**.
1. Select **Save changes**.
To set as default (or remove the default) from [compliance framework report](../../user/compliance/compliance_center/compliance_frameworks_report.md#compliance-frameworks-report):
To set as default (or remove the default) from [compliance framework report](../compliance/compliance_center/compliance_frameworks_report.md#compliance-frameworks-report):
1. On the left sidebar, select **Search or go to** and find your group.
1. Select **Secure > Compliance center**.
@ -89,4 +89,4 @@ To set as default (or remove the default) from [compliance framework report](../
## Remove a compliance framework from a project
To remove a compliance framework from one or multiple project in a group, remove the compliance framework through the
[Compliance projects report](../../user/compliance/compliance_center/compliance_projects_report.md#remove-a-compliance-framework-from-projects-in-a-group).
[Compliance projects report](../compliance/compliance_center/compliance_projects_report.md#remove-a-compliance-framework-from-projects-in-a-group).

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class BackfillStatusCheckResponsesProjectId < BackfillDesiredShardingKeyJob
operation_name :backfill_status_check_responses_project_id
feature_category :compliance_management
end
end
end

View File

@ -6,7 +6,18 @@ module Gitlab
PATH_TRAVERSAL_MESSAGE = 'Potential path traversal attempt detected'
# Query param names known to have string parts detected as path traversal even though
# they are valid genuine requests
EXCLUDED_QUERY_PARAM_NAMES = %w[search search_title term name filter filter_projects note body].freeze
EXCLUDED_QUERY_PARAM_NAMES = %w[
search
search_title
term
name
filter
filter_projects
note
body
commit_message
content
].freeze
def initialize(app)
@app = app

View File

@ -5,8 +5,9 @@ module Gitlab
class SetIpAddress
def call(_worker_class, job, _queue)
return yield if Feature.disabled?(:sidekiq_ip_address) # rubocop: disable Gitlab/FeatureFlagWithoutActor -- not applicable
return yield unless job.key?('ip_address_state')
::Gitlab::IpAddressState.with(job['meta.remote_ip']) do # rubocop: disable CodeReuse/ActiveRecord -- Non-AR
::Gitlab::IpAddressState.with(job['ip_address_state']) do # rubocop: disable CodeReuse/ActiveRecord -- Non-AR
yield
end
end

View File

@ -24062,6 +24062,9 @@ msgstr ""
msgid "GlobalSearch|Help"
msgstr ""
msgid "GlobalSearch|I'm looking for"
msgstr ""
msgid "GlobalSearch|In this project"
msgstr ""
@ -24086,9 +24089,6 @@ msgstr ""
msgid "GlobalSearch|Issues assigned to me"
msgstr ""
msgid "GlobalSearch|Im looking for"
msgstr ""
msgid "GlobalSearch|Labels"
msgstr ""

View File

@ -92,7 +92,8 @@ module RuboCop
def create_todos_retaining_exclusions(inspected_cop_config)
inspected_cop_config.each do |cop_name, config|
todo = @todos[cop_name]
todo.add_files(config.fetch('Exclude', []).grep(RETAIN_EXCLUSIONS))
excluded_files = config['Exclude'] || []
todo.add_files(excluded_files.grep(RETAIN_EXCLUSIONS))
end
end

View File

@ -87,7 +87,6 @@ docker buildx build \
--build-arg=CHROME_VERSION="${CHROME_VERSION}" \
--build-arg=DOCKER_VERSION="${DOCKER_VERSION}" \
--build-arg=RUBY_VERSION="${RUBY_VERSION}" \
--build-arg=BUNDLER_VERSION="${BUNDLER_VERSION}" \
--build-arg=BUILD_OS="${BUILD_OS}" \
--build-arg=OS_VERSION="${OS_VERSION}" \
--build-arg=QA_BUILD_TARGET="${QA_BUILD_TARGET}" \

View File

@ -13,7 +13,7 @@ import {
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_SOURCE_BRANCH,
TOKEN_TYPE_TARGET_BRANCH,
TOKEN_TYPE_MR_ASSIGNEE,
TOKEN_TYPE_ASSIGNEE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { mergeRequestListTabs } from '~/vue_shared/issuable/list/constants';
import { getSortOptions } from '~/issues/list/utils';
@ -109,7 +109,7 @@ describe('Merge requests list app', () => {
it('does not have preloaded users when gon.current_user_id does not exist', () => {
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_MR_ASSIGNEE },
{ type: TOKEN_TYPE_ASSIGNEE },
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers: [] },
{ type: TOKEN_TYPE_DRAFT },
{ type: TOKEN_TYPE_MILESTONE },
@ -121,7 +121,7 @@ describe('Merge requests list app', () => {
describe('when all tokens are available', () => {
const urlParams = {
mr_assignee_username: 'bob',
assignee_username: 'bob',
draft: 'yes',
milestone_title: 'milestone',
'target_branches[]': 'branch-a',
@ -153,7 +153,7 @@ describe('Merge requests list app', () => {
];
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_MR_ASSIGNEE },
{ type: TOKEN_TYPE_ASSIGNEE },
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
{ type: TOKEN_TYPE_DRAFT },
{ type: TOKEN_TYPE_MILESTONE },
@ -164,7 +164,7 @@ describe('Merge requests list app', () => {
it('pre-displays tokens that are in the url search parameters', () => {
expect(findIssuableList().props('initialFilterValue')).toMatchObject([
{ type: TOKEN_TYPE_MR_ASSIGNEE },
{ type: TOKEN_TYPE_ASSIGNEE },
{ type: TOKEN_TYPE_DRAFT },
{ type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_TARGET_BRANCH },

View File

@ -1,9 +1,11 @@
import { GlListboxItem, GlSprintf, GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CommandsOverviewDropdown from '~/super_sidebar/components/global_search/command_palette/command_overview_dropdown.vue';
import { mockTracking } from 'helpers/tracking_helper';
describe('CommandsOverviewDropdown', () => {
let wrapper;
let trackingSpy;
const createComponent = () => {
wrapper = shallowMountExtended(CommandsOverviewDropdown, {
@ -41,13 +43,14 @@ describe('CommandsOverviewDropdown', () => {
findItems().wrappers.map((w) => w.find('[data-testid="listbox-item-text"]').text());
beforeEach(() => {
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
createComponent();
});
describe('template', () => {
it('renders header', () => {
expect(findDropdown().find('[data-testid="listbox-header-text"]').text()).toBe(
'Im looking for',
"I'm looking for",
);
});
@ -65,5 +68,16 @@ describe('CommandsOverviewDropdown', () => {
findDropdown().vm.$emit('select', '@');
expect(wrapper.emitted('selected')).toEqual([['@']]);
});
it('tracks on shown event', () => {
findDropdown().vm.$emit('shown');
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
'click_commands_sub_menu_in_command_palette',
expect.anything(),
);
});
});
});

View File

@ -84,4 +84,15 @@ describe('FrequentlyVisitedGroups', () => {
expect(spy).toHaveBeenCalledTimes(1);
});
describe('events', () => {
beforeEach(() => {
createComponent();
});
it('emits action on click', () => {
findFrequentItems().vm.$emit('action');
expect(wrapper.emitted('action')).toStrictEqual([['FREQUENTLY_VISITED_GROUPS_HANDLE']]);
});
});
});

View File

@ -6,6 +6,7 @@ import FrequentProjects from '~/super_sidebar/components/global_search/component
import createMockApollo from 'helpers/mock_apollo_helper';
import currentUserFrecentProjectsQuery from '~/super_sidebar/graphql/queries/current_user_frecent_projects.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { FREQUENTLY_VISITED_PROJECTS_HANDLE } from '~/super_sidebar/components/global_search/command_palette/constants';
import { frecentProjectsMock } from '../../../mock_data';
Vue.use(VueApollo);
@ -84,4 +85,15 @@ describe('FrequentlyVisitedProjects', () => {
expect(spy).toHaveBeenCalledTimes(1);
});
describe('events', () => {
beforeEach(() => {
createComponent();
});
it('emits action on click', () => {
findFrequentItems().vm.$emit('action');
expect(wrapper.emitted('action')).toStrictEqual([[FREQUENTLY_VISITED_PROJECTS_HANDLE]]);
});
});
});

View File

@ -5,9 +5,15 @@ import GlobalSearchDefaultPlaces from '~/super_sidebar/components/global_search/
import FrequentProjects from '~/super_sidebar/components/global_search/components/frequent_projects.vue';
import FrequentGroups from '~/super_sidebar/components/global_search/components/frequent_groups.vue';
import GlobalSearchDefaultIssuables from '~/super_sidebar/components/global_search/components/global_search_default_issuables.vue';
import { mockTracking } from 'helpers/tracking_helper';
import {
FREQUENTLY_VISITED_PROJECTS_HANDLE,
FREQUENTLY_VISITED_GROUPS_HANDLE,
} from '~/super_sidebar/components/global_search/command_palette/constants';
describe('GlobalSearchDefaultItems', () => {
let wrapper;
let trackingSpy;
const createComponent = () => {
wrapper = shallowMount(GlobalSearchDefaultItems);
@ -23,6 +29,7 @@ describe('GlobalSearchDefaultItems', () => {
});
beforeEach(() => {
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
createComponent();
});
@ -73,4 +80,28 @@ describe('GlobalSearchDefaultItems', () => {
expect(groups.classes()).toEqual(['gl-mt-3']);
});
});
describe('events', () => {
it('tracks internal event on default projects component', () => {
findProjects().vm.$emit('action', FREQUENTLY_VISITED_PROJECTS_HANDLE);
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
'click_frequent_project_in_command_palette',
expect.anything(),
);
});
it('tracks internal event on default group component', () => {
findProjects().vm.$emit('action', FREQUENTLY_VISITED_GROUPS_HANDLE);
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(
undefined,
'click_frequent_group_in_command_palette',
expect.anything(),
);
});
});
});

View File

@ -87,6 +87,34 @@ RSpec.describe Resolvers::ProjectMergeRequestsResolver do
expect(result).to contain_exactly(merge_request)
end
it 'returns error when assignee username and wildcard id are used' do
expect_graphql_error_to_be_created(GraphQL::Schema::Validator::ValidationFailedError,
'Only one of [reviewerUsername, reviewerWildcardId] arguments is allowed at the same time.') do
resolve_mr(project, reviewer_username: current_user.username, reviewer_wildcard_id: 'ANY')
end
end
end
context 'with assignee wildcard param' do
it 'filters merge requests by NONE wildcard' do
result = resolve_mr(project, assignee_wildcard_id: 'NONE')
expect(result).to contain_exactly(merge_request2)
end
it 'filters merge requests by ANY wildcard' do
result = resolve_mr(project, assignee_wildcard_id: 'ANY')
expect(result).to contain_exactly(merge_request)
end
it 'returns error when assignee username and wildcard id are used' do
expect_graphql_error_to_be_created(GraphQL::Schema::Validator::ValidationFailedError,
'Only one of [assigneeUsername, assigneeWildcardId] arguments is allowed at the same time.') do
resolve_mr(project, assignee_username: current_user.username, assignee_wildcard_id: 'ANY')
end
end
end
context 'with milestone wildcard param' do

View File

@ -356,6 +356,7 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
:updated_before,
:author_username,
:assignee_username,
:assignee_wildcard_id,
:reviewer_username,
:reviewer_wildcard_id,
:review_state,

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillStatusCheckResponsesProjectId,
feature_category: :compliance_management,
schema: 20240611142348 do
include_examples 'desired sharding key backfill job' do
let(:batch_table) { :status_check_responses }
let(:backfill_column) { :project_id }
let(:backfill_via_table) { :merge_requests }
let(:backfill_via_column) { :target_project_id }
let(:backfill_via_foreign_key) { :merge_request_id }
end
end

View File

@ -4,12 +4,14 @@ require 'spec_helper'
RSpec.describe Gitlab::SidekiqMiddleware::SetIpAddress, feature_category: :system_access do
let(:worker) { instance_double(ApplicationWorker) }
let(:job) { { 'meta.remote_ip' => ip_address } }
let(:job) { { 'ip_address_state' => ip_address } }
let(:queue) { 'queue1' }
let(:ip_address) { '1.1.1.1' }
describe '#call' do
it 'sets the IP address in the context' do
it 'sets the IP address based on ip_address_state' do
expect(::Gitlab::IpAddressState).to receive(:with).once.and_call_original
described_class.new.call(worker, job, queue) do
expect(::Gitlab::IpAddressState.current).to eq(ip_address)
end
@ -17,10 +19,26 @@ RSpec.describe Gitlab::SidekiqMiddleware::SetIpAddress, feature_category: :syste
expect(::Gitlab::IpAddressState.current).to eq(nil)
end
context 'when the IP address is absent' do
context 'when the ip_address_state key is absent' do
let(:job) { {} }
it 'does not set the IP address' do
expect(::Gitlab::IpAddressState).not_to receive(:with).with(ip_address)
described_class.new.call(worker, job, queue) do
expect(::Gitlab::IpAddressState.current).to eq(nil)
end
expect(::Gitlab::IpAddressState.current).to eq(nil)
end
end
context 'when ip_address_state value is nil' do
let(:job) { { 'ip_address_state' => nil } }
it 'sets IP address to be nil' do
expect(::Gitlab::IpAddressState).to receive(:with).once.and_call_original
described_class.new.call(worker, job, queue) do
expect(::Gitlab::IpAddressState.current).to eq(nil)
end
@ -37,6 +55,8 @@ RSpec.describe Gitlab::SidekiqMiddleware::SetIpAddress, feature_category: :syste
context 'when the IP address is present' do
it 'does not set the IP address' do
expect(::Gitlab::IpAddressState).not_to receive(:with).with(ip_address)
described_class.new.call(worker, job, queue) do
expect(::Gitlab::IpAddressState.current).to eq(nil)
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration!
RSpec.describe QueueBackfillStatusCheckResponsesProjectId, feature_category: :compliance_management do
let!(:batched_migration) { described_class::MIGRATION }
it 'schedules a new batched migration' do
reversible_migration do |migration|
migration.before -> {
expect(batched_migration).not_to have_scheduled_batched_migration
}
migration.after -> {
expect(batched_migration).to have_scheduled_batched_migration(
table_name: :status_check_responses,
column_name: :id,
interval: described_class::DELAY_INTERVAL,
batch_size: described_class::BATCH_SIZE,
sub_batch_size: described_class::SUB_BATCH_SIZE,
gitlab_schema: :gitlab_main_cell,
job_arguments: [
:project_id,
:merge_requests,
:target_project_id,
:merge_request_id
]
)
}
end
end
end

View File

@ -98,6 +98,22 @@ RSpec.describe RuboCop::Formatter::TodoFormatter, feature_category: :tooling do
YAML
end
context 'with empty exclusions' do
before do
todo_dir.write('C/EmptyList', <<~YAML)
---
C/EmptyList:
Exclude:
YAML
todo_dir.inspect_all
end
it 'does not raise an error' do
expect { run_formatter }.not_to raise_error
end
end
context 'with existing HAML exclusions' do
before do
todo_dir.write('B/TooManyOffenses', <<~YAML)

View File

@ -584,6 +584,23 @@ RSpec.describe ApplicationWorker, feature_category: :shared do
end
end
describe '.with_ip_address_state' do
around do |example|
Sidekiq::Testing.fake!(&example)
end
let(:ip_address) { '1.1.1.1' }
it 'sets IP state' do
allow(::Gitlab::IpAddressState).to receive(:current).and_return(ip_address)
worker.with_ip_address_state.perform_async
expect(Sidekiq::Queues[worker.queue].first).to include('ip_address_state' => ip_address)
expect(Sidekiq::Queues[worker.queue].length).to eq(1)
end
end
context 'when using perform_async/in/at' do
let(:shard_pool) { 'dummy_pool' }
let(:shard_name) { 'shard_name' }

View File

@ -8,7 +8,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2
github.com/BurntSushi/toml v1.4.0
github.com/alecthomas/chroma/v2 v2.14.0
github.com/aws/aws-sdk-go v1.53.7
github.com/aws/aws-sdk-go v1.53.16
github.com/disintegration/imaging v1.6.2
github.com/getsentry/raven-go v0.2.0
github.com/golang-jwt/jwt/v5 v5.2.1

View File

@ -96,8 +96,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/aws/aws-sdk-go v1.44.256/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aws/aws-sdk-go v1.53.7 h1:ZSsRYHLRxsbO2rJR2oPMz0SUkJLnBkN+1meT95B6Ixs=
github.com/aws/aws-sdk-go v1.53.7/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go v1.53.16 h1:8oZjKQO/ml1WLUZw5hvF7pvYjPf8o9f57Wldoy/q9Qc=
github.com/aws/aws-sdk-go v1.53.16/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
github.com/aws/aws-sdk-go-v2 v1.25.3 h1:xYiLpZTQs1mzvz5PaI6uR0Wh57ippuEthxS4iK5v0n0=
github.com/aws/aws-sdk-go-v2 v1.25.3/go.mod h1:35hUlJVYd+M++iLI3ALmVwMOyRYMmRqUXpTtRGW+K9I=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 h1:gTK2uhtAPtFcdRRJilZPx8uJLL2J85xK11nKtWL0wfU=