Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									a5efa544eb
								
							
						
					
					
						commit
						bd15a45eeb
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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: | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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") | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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', | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|     ) { | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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|I’m 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"> | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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' | ||||
| 
 | ||||
|  |  | |||
|  | @ -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' | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -0,0 +1 @@ | |||
| eaab626b81ea7df21bdbbcf3f0da019bce66d6bae83a2c18fcec732d22d52c7d | ||||
|  | @ -0,0 +1 @@ | |||
| f2592bf6d74deffbef5626e33856f33af256a3a5f3074e8cfa31eb6409362f9b | ||||
|  | @ -0,0 +1 @@ | |||
| ec17602042e8e11da5f1faa7b58659fb455dbf1a2dfdd8cfae90fac5f7ee87f6 | ||||
|  | @ -0,0 +1 @@ | |||
| 2c2d509c9019c35b59e2231f8c473fd3a0af60bcebf45aa4c0bfa10a590614e5 | ||||
|  | @ -0,0 +1 @@ | |||
| aea0e6c1b36b4ec32f8bbb22c1e2eab6a4f7a060f486382267fc16e355dd72c5 | ||||
|  | @ -0,0 +1 @@ | |||
| 4844652782e0966eeb1ce935a21a06291f534623bf8c91f9c2f8c171bd9d1645 | ||||
|  | @ -0,0 +1 @@ | |||
| 5f92f0daf25ba601f64916028a9bb980d37a28dc7b0c9c513d1e8bb8aad706b3 | ||||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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. | | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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? | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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). | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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|I’m looking for" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "GlobalSearch|Labels" | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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}" \ | ||||
|  |  | |||
|  | @ -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 }, | ||||
|  |  | |||
|  | @ -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( | ||||
|         'I’m 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(), | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -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']]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -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]]); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -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(), | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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' } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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= | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue