-
+
+
-
+
{{ $options.i18n.COMMIT_MESSAGE_HINT }}
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 02084333225..091bd046921 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -253,13 +253,13 @@ export default {
v-safe-html:[$options.safeHtmlConfig]="commitData.titleHtml"
:href="commitData.commitPath"
:title="commitData.message"
- class="str-truncated-100 tree-commit-link gl-text-gray-600"
+ class="str-truncated-100 tree-commit-link gl-text-subtle"
/>
-
+ |
diff --git a/app/assets/javascripts/search/topbar/components/search_type_indicator.vue b/app/assets/javascripts/search/topbar/components/search_type_indicator.vue
index ecabb89a74e..87c6bce5faf 100644
--- a/app/assets/javascripts/search/topbar/components/search_type_indicator.vue
+++ b/app/assets/javascripts/search/topbar/components/search_type_indicator.vue
@@ -125,7 +125,7 @@ export default {
-
+
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index 690460fdae6..0ea2937f93d 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -268,7 +268,7 @@ export default {
{{ $options.i18n.unavailableText }}
diff --git a/app/assets/javascripts/todos/components/todos_app.vue b/app/assets/javascripts/todos/components/todos_app.vue
index 4aa4c31bb38..de240b42583 100644
--- a/app/assets/javascripts/todos/components/todos_app.vue
+++ b/app/assets/javascripts/todos/components/todos_app.vue
@@ -8,6 +8,7 @@ import Tracking from '~/tracking';
import {
INSTRUMENT_TAB_LABELS,
INSTRUMENT_TODO_FILTER_CHANGE,
+ INSTRUMENT_TODO_ITEM_CLICK,
STATUS_BY_TAB,
} from '~/todos/constants';
import getTodosQuery from './queries/get_todos.query.graphql';
@@ -165,6 +166,9 @@ export default {
onClick: async () => {
hide();
await this.$apollo.mutate({ mutation, variables: { todoId } });
+ this.track(INSTRUMENT_TODO_ITEM_CLICK, {
+ label: markedAsDone ? 'undo_mark_done' : 'undo_mark_pending',
+ });
this.updateAllQueries(false);
},
},
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_list.vue b/app/assets/javascripts/usage_quotas/storage/components/project_list.vue
index de38c133f58..1ab0655c036 100644
--- a/app/assets/javascripts/usage_quotas/storage/components/project_list.vue
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_list.vue
@@ -152,7 +152,7 @@ export default {
-
+
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
index 2332e32ec49..79b97802111 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue
@@ -781,7 +781,7 @@ export default {
{{ __('Merge details') }}
-
+
-
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue
index 566fef4075c..c40fadb8512 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue
+++ b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue
@@ -41,7 +41,7 @@ export default {
{{ title }}
- @{{ username }}
+ @{{ username }}
{{ fullName }}
- @{{ name }}
+ @{{ name }}
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/project_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/project_item.vue
index e51cd6b29eb..5a573b2c275 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/project_item.vue
+++ b/app/assets/javascripts/vue_shared/components/list_selector/project_item.vue
@@ -45,7 +45,7 @@ export default {
/>
{{ name }}
- {{ data.nameWithNamespace }}
+ {{ data.nameWithNamespace }}
{{ name }}
- @{{ username }}
+ @{{ username }}
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 90f37e35185..812c294683a 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -105,7 +105,7 @@ export default {
return this.getNoteableData.noteableType === 'MergeRequest';
},
iconBgClass() {
- return ICON_COLORS[this.note.system_note_icon_name] || 'gl-bg-gray-50 gl-text-gray-600';
+ return ICON_COLORS[this.note.system_note_icon_name] || 'gl-bg-gray-50 gl-text-subtle';
},
systemNoteIconName() {
let icon = this.note.system_note_icon_name;
diff --git a/app/assets/javascripts/vue_shared/components/notes/timeline_icon.vue b/app/assets/javascripts/vue_shared/components/notes/timeline_icon.vue
index 1507902b48f..612c0c12ef3 100644
--- a/app/assets/javascripts/vue_shared/components/notes/timeline_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/timeline_icon.vue
@@ -1,6 +1,6 @@
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index c413fa90638..29074050f8c 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -130,12 +130,12 @@ export default {
{{ legendText }}
-
+
{{ __('Sorry, no projects matched your search') }}
{{ __('Enter at least three characters to search') }}
diff --git a/app/assets/javascripts/vue_shared/components/registry/history_item.vue b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
index c36f686d01a..f4529c3e3a2 100644
--- a/app/assets/javascripts/vue_shared/components/registry/history_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/history_item.vue
@@ -20,7 +20,7 @@ export default {
diff --git a/app/assets/javascripts/work_items/components/design_management/design_version_dropdown.vue b/app/assets/javascripts/work_items/components/design_management/design_version_dropdown.vue
index f3f142d1476..3c5fe06c878 100644
--- a/app/assets/javascripts/work_items/components/design_management/design_version_dropdown.vue
+++ b/app/assets/javascripts/work_items/components/design_management/design_version_dropdown.vue
@@ -119,7 +119,7 @@ export default {
{{ versionText(item) }}
-
+
{{ getAuthorName(item.author) }}
diff --git a/app/assets/javascripts/work_items/components/work_item_crm_contacts.vue b/app/assets/javascripts/work_items/components/work_item_crm_contacts.vue
index bd6c71ee886..eae4f239ee8 100644
--- a/app/assets/javascripts/work_items/components/work_item_crm_contacts.vue
+++ b/app/assets/javascripts/work_items/components/work_item_crm_contacts.vue
@@ -331,7 +331,7 @@ export default {
{{ organizationName }}
`fieldType` | [`CustomFieldType!`](#customfieldtype) | Type of custom field. |
| `groupPath` | [`ID!`](#id) | Group path where the custom field is created. |
| `name` | [`String!`](#string) | Name of the custom field. |
+| `selectOptions` | [`[CustomFieldSelectOptionInput!]`](#customfieldselectoptioninput) | Available options for a select field. |
#### Fields
@@ -3991,6 +3992,7 @@ Input type: `CustomFieldUpdateInput`
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `id` | [`IssuablesCustomFieldID!`](#issuablescustomfieldid) | Global ID of the custom field. |
| `name` | [`String!`](#string) | Name of the custom field. |
+| `selectOptions` | [`[CustomFieldSelectOptionInput!]`](#customfieldselectoptioninput) | Available options for a select field. |
#### Fields
@@ -43097,6 +43099,17 @@ Attributes for defining a CI/CD variable.
| `mergedBefore` | [`Date`](#date) | Merge requests merged before the date (inclusive). |
| `targetBranch` | [`String`](#string) | Filter compliance violations by target branch. |
+### `CustomFieldSelectOptionInput`
+
+Attributes for the custom field select option.
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `id` | [`IssuablesCustomFieldSelectOptionID`](#issuablescustomfieldselectoptionid) | Global ID of the custom field select option to update. Creates a new record if not provided. |
+| `value` | [`String!`](#string) | Value of the custom field select option. |
+
### `DastProfileCadenceInput`
Represents DAST Profile Cadence.
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index 84c4561bba6..df80eb6a2e7 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -32,7 +32,7 @@ This access can also [be restricted](#limit-job-token-scope-for-public-or-intern
| Feature | Additional details |
|-------------------------------------------------------------------------------------------------------|--------------------|
| [Container registry API](../../api/container_registry.md) | The token is scoped to the container registry of the job's project only. |
-| [Container registry](../../user/packages/container_registry/build_and_push_images.md#use-gitlab-cicd) | The `$CI_REGISTRY_PASSWORD` [predefined variable](../variables/predefined_variables.md) is the CI/CD job token. |
+| [Container registry](../../user/packages/container_registry/build_and_push_images.md#use-gitlab-cicd) | The `$CI_REGISTRY_PASSWORD` [predefined variable](../variables/predefined_variables.md) is the CI/CD job token. Both are scoped to the container registry of the job's project only. |
| [Deployments API](../../api/deployments.md) | `GET` requests are public by default. |
| [Environments API](../../api/environments.md) | `GET` requests are public by default. |
| [Job artifacts API](../../api/job_artifacts.md#get-job-artifacts) | `GET` requests are public by default. |
@@ -69,7 +69,7 @@ jobs.
## Control job token access to your project
-You can control which groups or projects can use a job token to authenticate and access your project's resources.
+You can control which groups or projects can use a job token to authenticate and access some of your project's resources.
By default, job token access is restricted to only CI/CD jobs that run in pipelines in
your project. To allow another group or project to authenticate with a job token from the other
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index ecbe476c7dc..3f5cd8b3976 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -207,7 +207,7 @@ refresh_service.execute(oldrev, newrev, ref)
See ["Why is it bad style to `rescue Exception => e` in Ruby?"](https://stackoverflow.com/questions/10048173/why-is-it-bad-style-to-rescue-exception-e-in-ruby).
-This rule is [enforced automatically by RuboCop](https://gitlab.com/gitlab-org/gitlab-foss/blob/8-4-stable/.rubocop.yml#L911-914)._
+This rule is [enforced automatically by RuboCop](https://gitlab.com/gitlab-org/gitlab-foss/blob/8-4-stable/.rubocop.yml#L911-914).
## Do not use inline JavaScript in views
diff --git a/doc/subscriptions/bronze_starter.md b/doc/subscriptions/bronze_starter.md
index b7d9bebc994..bcb4ba24618 100644
--- a/doc/subscriptions/bronze_starter.md
+++ b/doc/subscriptions/bronze_starter.md
@@ -66,7 +66,7 @@ the tiers are no longer mentioned in GitLab documentation:
- [Full code quality reports in the code quality tab](../ci/testing/code_quality.md#pipeline-details-view)
- [Merge request approvals](../user/project/merge_requests/approvals/index.md)
- [Multiple assignees](../user/project/merge_requests/index.md#assign-a-user-to-a-merge-request)
- - [Approval rule information for reviewers](../user/project/merge_requests/reviews/index.md#see-how-reviewers-map-to-approval-rules)
+ - [Approval rule information for reviewers](../user/project/merge_requests/reviews/index.md#request-a-review)
- [Required Approvals](../user/project/merge_requests/approvals/index.md#required-approvals)
- [Code Owners as eligible approvers](../user/project/merge_requests/approvals/rules.md#code-owners-as-eligible-approvers)
- [Approval rules](../user/project/merge_requests/approvals/rules.md) features
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index 9d656befdef..9de4a40fb2c 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -196,28 +196,6 @@ We've deprecated the use of `ref` and `sha` in API calls to `GET /projects/:id/c
-### Breaking change to the Maven repository group permissions
-
-
-
-- Announced in GitLab 16.6
-- Removal in GitLab 18.0 ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
-- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/393933).
-
-
-
-The Maven repository exposes an API endpoint at the group level that allows Maven clients to download files from a specific package. The package finder first locates the package within the group, and then finds the file within the package.
-However, there is a limitation that affects duplicate package names hosted in different projects. The Maven package finder always returns the most recent package, but the "most recent" filter depends on user permissions. It is possible for a user with different permissions in different projects to download the wrong Maven package.
-
-In GitLab 18.0, the package finder logic will be fixed so that the "most recent" package is the last updated name and version of a package in a group. User permissions will be checked after the most recent package is found.
-After the change, download requests for users without correct permissions will be rejected. If your workflow depends on the current bugged behavior, this fix will introduce a breaking change.
-
-The change will be introduced in GitLab 16.6 behind a feature flag. If you are interested in enabling the feature flag for your group, leave a comment in [issue 393933](https://gitlab.com/gitlab-org/gitlab/-/issues/393933).
-
-
-
-
-
### CodeClimate-based Code Quality scanning will be removed
diff --git a/doc/user/project/merge_requests/reviews/data_usage.md b/doc/user/project/merge_requests/reviews/data_usage.md
index cf6ab5df4f3..14a2727ca6b 100644
--- a/doc/user/project/merge_requests/reviews/data_usage.md
+++ b/doc/user/project/merge_requests/reviews/data_usage.md
@@ -1,48 +1,14 @@
---
-stage: AI-powered
-group: AI Model Validation
-info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
-ignore_in_report: true
+redirect_to: 'index.md'
+remove_date: '2025-02-03'
---
-# Suggested Reviewers data usage
+
-DETAILS:
-**Tier:** Ultimate
-**Offering:** GitLab.com
+This document was moved to [another location](index.md).
-## How it works
-
-Suggested Reviewers is the first user-facing GitLab machine learning (ML) powered feature. It leverages a project's contribution graph to generate suggestions. This data already exists within GitLab including merge request metadata, source code files, and GitLab user account metadata.
-
-### Enabling the feature
-
-When a Project Maintainer or Owner enables Suggested Reviewers in project settings, GitLab kicks off a data extraction job for the project which leverages the Merge Request API to understand pattern of review including recency, domain experience, and frequency to suggest an appropriate reviewer. If projects do not use the [merge request approval process](../approvals/index.md) or do not have any historical merge request data, Suggested Reviewers cannot suggest reviewers.
-
-This data extraction job can take a few hours to complete (possibly up to a day), which is largely dependent on the size of the project. The process is automated and no action is needed during this process. Once data extraction is complete, you start getting suggestions in merge requests.
-
-### Generating suggestions
-
-Once Suggested Reviewers is enabled and the data extraction is complete, new merge requests or new commits to existing merge requests automatically trigger a Suggested Reviewers ML model inference and generate up to 5 suggested reviewers. These suggestions are contextual to the changes in the merge request. Additional commits to merge requests may change the reviewer suggestions, which are automatically updated in the reviewer dropdown list.
-
-## Progressive enhancement
-
-This feature is designed as a progressive enhancement to the existing GitLab Reviewers functionality. The GitLab Reviewer UI only offers suggestions if the ML engine is able to provide a recommendation. In the event of an issue or model inference failure, the feature gracefully degrades. At no point with the usage of Suggested Reviewers prevent a user from being able to manually set a reviewer.
-
-## Model Accuracy
-
-Organizations use many different processes for code review. Some focus on senior engineers reviewing junior engineer's code, others have hierarchical organizational structure based reviews. Suggested Reviewers is focused on contextual reviewers based on historical merge request activity by users. While we continue evolving the underlying ML model to better serve various code review use cases and processes Suggested Reviewers does not replace the usage of other code review features like Code Owners and [Approval Rules](../approvals/rules.md). Reviewer selection is highly subjective therefore, we do not expect Suggested Reviewers to provide perfect suggestions every time.
-
-Through analysis of beta customer usage, we find that the Suggested Reviewers ML model provides suggestions that are adopted in 60% of cases. We plan to introduce a feedback mechanism into the Suggested Reviewers feature in the future to allow users to flag bad reviewer suggestions to help improve the model. Additionally we plan to offer an opt-in feature in the future which allows the model to use your project's data for training the underlying model.
-
-## Off by default
-
-Suggested Reviewers is off by default and requires a Project Owner or Admin to enable the feature.
-
-## Data privacy
-
-Suggested Reviewers operates completely within the GitLab.com infrastructure providing the same level of [privacy](https://about.gitlab.com/privacy/) and [security](https://about.gitlab.com/security/) of any other feature of GitLab.com.
-
-No new additional data is collected to enable this feature. GitLab infers your merge request against a trained machine learning model. The content of your source code is not used as training data. Your data also never leaves GitLab.com, all training and inference is done within GitLab.com infrastructure.
-
-[Read more about the security of GitLab.com](https://about.gitlab.com/security/faq/)
+
+
+
+
+
diff --git a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v15_9.png b/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v15_9.png
deleted file mode 100644
index 6839c675625..00000000000
Binary files a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_form_v15_9.png and /dev/null differ
diff --git a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v15_9.png b/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v15_9.png
deleted file mode 100644
index c7942d1e36d..00000000000
Binary files a/doc/user/project/merge_requests/reviews/img/reviewer_approval_rules_sidebar_v15_9.png and /dev/null differ
diff --git a/doc/user/project/merge_requests/reviews/img/select_good_reviewers_v17_5.png b/doc/user/project/merge_requests/reviews/img/select_good_reviewers_v17_5.png
new file mode 100644
index 00000000000..be6498031e7
Binary files /dev/null and b/doc/user/project/merge_requests/reviews/img/select_good_reviewers_v17_5.png differ
diff --git a/doc/user/project/merge_requests/reviews/img/suggested_reviewers_v16_3.png b/doc/user/project/merge_requests/reviews/img/suggested_reviewers_v16_3.png
deleted file mode 100644
index 9c3ecebd395..00000000000
Binary files a/doc/user/project/merge_requests/reviews/img/suggested_reviewers_v16_3.png and /dev/null differ
diff --git a/doc/user/project/merge_requests/reviews/index.md b/doc/user/project/merge_requests/reviews/index.md
index bcbba872d1f..632fa24d4e6 100644
--- a/doc/user/project/merge_requests/reviews/index.md
+++ b/doc/user/project/merge_requests/reviews/index.md
@@ -52,36 +52,45 @@ of a merge request. Each **Reviewer** shows the status to the right of the user'
## Request a review
-To assign a reviewer to a merge request, in a text area in
-the merge request, use the `/assign_reviewer @user`
-[quick action](../../quick_actions.md#issues-merge-requests-and-epics), or:
+> - Enhanced reviewer drawer [introduced](https://gitlab.com/groups/gitlab-org/-/epics/12878) in GitLab 17.5 [with a flag](../../../../administration/feature_flags.md) named `reviewer_assign_drawer`.
+> - [Enabled](https://gitlab.com/gitlab-org/gitlab/-/issues/467205) on GitLab.com and self-managed in GitLab 17.5.
+
+When you've finished preparing your changes, it's time to request a review. To assign a reviewer to your merge request,
+either use the `/assign_reviewer @user`
+[quick action](../../quick_actions.md#issues-merge-requests-and-epics) in any text field, or:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Code > Merge requests** and find your merge request.
1. Select the title of the merge request to view it.
-1. On the right sidebar, in the **Reviewers** section, select **Edit**.
-1. Search for the user you want to assign, and select the user.
+1. On the right sidebar, in the **Reviewers** section:
+ - To find a specific reviewer by name, select **Edit**.
+ - In GitLab Premium and Ultimate, to find a reviewer
+ [who fulfills approval rules](#find-reviewers-who-fulfill-approval-rules), select **Assign** to
+ open the reviewer drawer.
GitLab adds the merge request to the user's review requests.
-### From multiple users
+### Find reviewers who fulfill approval rules
DETAILS:
**Tier:** Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
-To assign multiple reviewers to a merge request, in a text area in
-the merge request, use the `/assign_reviewer @user1 @user2`
-[quick action](../../quick_actions.md#issues-merge-requests-and-epics), or:
+GitLab Premium and Ultimate help you more quickly find the best reviewers for your merge request.
+Use the **Assign reviewers** drawer to filter lists of reviewers. See the Code Owners for the files
+changed in your merge request, and the users who satisfy your project's approval rules.
-1. On the left sidebar, select **Search or go to** and find your project.
-1. Select **Code > Merge requests** and find your merge request.
-1. Select the title of the merge request to view it.
-1. On the right sidebar, in the **Reviewers** section, select **Edit**.
-1. From the dropdown list, select all the users you want
- to assign to the merge request.
+In this example, the merge request requires 3 Code Owner approvals, but has none so far:
-To remove a reviewer, clear the user from the same dropdown list.
+
+
+1. To see optional approval rules or Code Owners, select **Optional approval rules** (**{chevron-lg-up}**) to show them.
+1. Next to the reviewer type you need, select **Edit**:
+ - **Code Owners** shows only the Code Owners for that file type.
+ - **Approval rules** shows only users who fulfill that approval rule.
+1. Select your desired reviewers. (GitLab Premium and Ultimate enable you to select multiple reviewers.)
+1. Repeat for each required **Code Owner** and **Approval rule** item.
+1. When you've selected your reviewers, on the top right, select **Close** (**{close}**) to hide the drawer.
### Re-request a review
@@ -196,43 +205,6 @@ another user with permission to merge the merge request can override this check:

-### See how reviewers map to approval rules
-
-DETAILS:
-**Tier:** Premium, Ultimate
-**Offering:** GitLab.com, Self-managed, GitLab Dedicated
-
-When you create a merge request, you want to request reviews from
-subject matter experts for the changes you're making. To decrease the number of
-review cycles for your merge request, consider requesting reviews from users
-listed in the project's approval rules.
-
-When you edit the **Reviewers** field in a merge request, GitLab shows you
-the matching [approval rule](../approvals/rules.md) below the name of each reviewer.
-[Code Owners](../../codeowners/index.md) display as `Codeowner` without any group detail.
-
-::Tabs
-
-:::TabTitle Create or edit a merge request
-
-1. When you create a new merge request, or edit an existing one, select **Reviewers**.
-1. Begin entering the name of your desired reviewer. Users who are Code Owners, or match an approval rule, show more information below the username:
-
- 
-
-:::TabTitle Reviewing a merge request
-
-1. On the left sidebar, select **Search or go to** and find your project.
-1. Select **Code > Merge requests**.
-1. Select your merge request.
-1. On the right sidebar, next to **Reviewers**, select **Edit**.
-1. Begin entering the name of your desired reviewer. Users who are Code Owners,
- or who match an approval rule, show more information below the username:
-
- 
-
-::EndTabs
-
## Download merge request changes
### As a diff
@@ -281,54 +253,6 @@ To download and apply the patch in a one-line CLI command using [`git am`](https
curl "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/000000.patch" | git am
```
-## Suggested Reviewers
-
-DETAILS:
-**Tier:** Ultimate
-**Offering:** GitLab.com
-
-> - [Introduced](https://gitlab.com/groups/gitlab-org/modelops/applied-ml/review-recommender/-/epics/3) in GitLab 15.4 as a [beta](../../../../policy/experiment-beta-support.md#beta) feature [with a flag](../../../../administration/feature_flags.md) named `suggested_reviewers_control`. Disabled by default.
-> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/368356) in GitLab 15.6.
-> - Beta designation [removed from the UI](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113058) in GitLab 15.10.
-> - Feature flag [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134728) in GitLab 16.6.
-
-GitLab uses machine learning to suggest reviewers for your merge request.
-
-To suggest reviewers, GitLab uses:
-
-- The changes in the merge request
-- The project's contribution graph
-
-Suggested Reviewers also integrates with Code Owners, profile status,
-and merge request rules. It helps you make a more informed decision when choosing
-reviewers who can meet your review criteria.
-
-
-
-For more information, see [Data usage in Suggested Reviewers](data_usage.md).
-
-### Enable Suggested Reviewers
-
-Enabling Suggested Reviewers triggers GitLab to create the machine learning model your
-project uses to generate reviewers. The larger your project, the longer
-this process can take. The model is usually ready to generate suggestions
-after a few hours.
-
-Prerequisites:
-
-- You have the Owner or Maintainer role for the project.
-
-To do this:
-
-1. On the left sidebar, select **Search or go to** and find your project.
-1. Select **Settings > Merge requests**.
-1. Scroll to **Suggested reviewers**, and select **Enable suggested reviewers**.
-1. Select **Save changes**.
-
-After you enable the feature, no action is needed. After the model is ready,
-recommendations populate the **Reviewer** dropdown list in the right-hand sidebar
-of a merge request with new commits.
-
## Associated features
Merge requests are related to these features:
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 12df7ae27bd..5d21d05ba5a 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -139,7 +139,6 @@ Configure your project's merge request settings:
- [Merge only if pipeline succeeds](../merge_requests/auto_merge.md).
- [Merge only when all threads are resolved](../merge_requests/index.md#prevent-merge-unless-all-threads-are-resolved).
- [Required associated issue from Jira](../../../integration/jira/issues.md#require-associated-jira-issue-for-merge-requests-to-be-merged).
- - [Suggested Reviewers](../merge_requests/reviews/index.md#suggested-reviewers)
- [**Delete source branch when merge request is accepted** option by default](#delete-the-source-branch-on-merge-by-default).
- Configure:
- [Suggested changes commit messages](../merge_requests/reviews/suggestions.md#configure-the-commit-message-for-applied-suggestions).
diff --git a/gems/activerecord-gitlab/Gemfile.lock b/gems/activerecord-gitlab/Gemfile.lock
index f84c03d1044..0cd3dfeb429 100644
--- a/gems/activerecord-gitlab/Gemfile.lock
+++ b/gems/activerecord-gitlab/Gemfile.lock
@@ -39,7 +39,7 @@ GEM
rack (3.1.8)
rainbow (3.1.1)
regexp_parser (2.8.1)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -102,4 +102,4 @@ DEPENDENCIES
sqlite3 (~> 1.6)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/click_house-client/Gemfile.lock b/gems/click_house-client/Gemfile.lock
index 2595783a79c..57bc5ca1a53 100644
--- a/gems/click_house-client/Gemfile.lock
+++ b/gems/click_house-client/Gemfile.lock
@@ -41,7 +41,7 @@ GEM
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.1)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -103,4 +103,4 @@ DEPENDENCIES
rubocop-rspec
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/csv_builder/Gemfile.lock b/gems/csv_builder/Gemfile.lock
index 34cdbefc0e3..0224cb0c4ed 100644
--- a/gems/csv_builder/Gemfile.lock
+++ b/gems/csv_builder/Gemfile.lock
@@ -37,7 +37,7 @@ GEM
rack (3.1.8)
rainbow (3.1.1)
regexp_parser (2.8.1)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -97,4 +97,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-backup-cli/Gemfile.lock b/gems/gitlab-backup-cli/Gemfile.lock
index e18402a8960..5515619ce38 100644
--- a/gems/gitlab-backup-cli/Gemfile.lock
+++ b/gems/gitlab-backup-cli/Gemfile.lock
@@ -133,7 +133,7 @@ GEM
rainbow (3.1.1)
rake (13.2.1)
regexp_parser (2.9.2)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
@@ -209,4 +209,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.27.1)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-database-load_balancing/Gemfile.lock b/gems/gitlab-database-load_balancing/Gemfile.lock
index 72dfcd4426d..6456e6389c0 100644
--- a/gems/gitlab-database-load_balancing/Gemfile.lock
+++ b/gems/gitlab-database-load_balancing/Gemfile.lock
@@ -205,7 +205,7 @@ GEM
regexp_parser (2.7.0)
request_store (1.5.1)
rack (>= 1.4)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -300,4 +300,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-database-lock_retries/Gemfile.lock b/gems/gitlab-database-lock_retries/Gemfile.lock
index be91aba6396..eccea123ca7 100644
--- a/gems/gitlab-database-lock_retries/Gemfile.lock
+++ b/gems/gitlab-database-lock_retries/Gemfile.lock
@@ -43,7 +43,7 @@ GEM
rack (3.1.8)
rainbow (3.1.1)
regexp_parser (2.8.2)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -102,4 +102,4 @@ DEPENDENCIES
rspec (~> 3.0)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-housekeeper/Gemfile.lock b/gems/gitlab-housekeeper/Gemfile.lock
index 9320bb314a1..b97641dbe01 100644
--- a/gems/gitlab-housekeeper/Gemfile.lock
+++ b/gems/gitlab-housekeeper/Gemfile.lock
@@ -74,7 +74,7 @@ GEM
rainbow (3.1.1)
rake (13.1.0)
regexp_parser (2.8.3)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -161,4 +161,4 @@ DEPENDENCIES
webmock
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-http/Gemfile.lock b/gems/gitlab-http/Gemfile.lock
index e1dc19fbd55..9bd26b341ce 100644
--- a/gems/gitlab-http/Gemfile.lock
+++ b/gems/gitlab-http/Gemfile.lock
@@ -125,7 +125,7 @@ GEM
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.1)
- rexml (3.3.8)
+ rexml (3.3.9)
rouge (4.3.0)
rspec (3.12.0)
rspec-core (~> 3.12.0)
@@ -220,4 +220,4 @@ DEPENDENCIES
webrick (~> 1.8)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-rspec/Gemfile.lock b/gems/gitlab-rspec/Gemfile.lock
index 95f8bd26835..21c123073bb 100644
--- a/gems/gitlab-rspec/Gemfile.lock
+++ b/gems/gitlab-rspec/Gemfile.lock
@@ -94,7 +94,7 @@ GEM
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.1)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -185,4 +185,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-rspec_flaky/Gemfile.lock b/gems/gitlab-rspec_flaky/Gemfile.lock
index fcd4dcd983e..bd7e7f1ee55 100644
--- a/gems/gitlab-rspec_flaky/Gemfile.lock
+++ b/gems/gitlab-rspec_flaky/Gemfile.lock
@@ -67,7 +67,7 @@ GEM
rack (3.1.8)
rainbow (3.1.1)
regexp_parser (2.8.1)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -143,4 +143,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-safe_request_store/Gemfile.lock b/gems/gitlab-safe_request_store/Gemfile.lock
index ad330c18f39..1e9d24962ea 100644
--- a/gems/gitlab-safe_request_store/Gemfile.lock
+++ b/gems/gitlab-safe_request_store/Gemfile.lock
@@ -41,7 +41,7 @@ GEM
regexp_parser (2.7.0)
request_store (1.5.1)
rack (>= 1.4)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -101,4 +101,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-schema-validation/Gemfile.lock b/gems/gitlab-schema-validation/Gemfile.lock
index 77074cd9233..c026c1ed473 100644
--- a/gems/gitlab-schema-validation/Gemfile.lock
+++ b/gems/gitlab-schema-validation/Gemfile.lock
@@ -54,7 +54,7 @@ GEM
rack (3.1.8)
rainbow (3.1.1)
regexp_parser (2.8.1)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -136,4 +136,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-secret_detection/Gemfile.lock b/gems/gitlab-secret_detection/Gemfile.lock
index 1dc9f0292a3..60185bb932f 100644
--- a/gems/gitlab-secret_detection/Gemfile.lock
+++ b/gems/gitlab-secret_detection/Gemfile.lock
@@ -62,7 +62,7 @@ GEM
re2 (2.4.3)
mini_portile2 (~> 2.8.5)
regexp_parser (2.8.2)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -148,4 +148,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/gitlab-utils/Gemfile.lock b/gems/gitlab-utils/Gemfile.lock
index f26c60565d7..342bef0b116 100644
--- a/gems/gitlab-utils/Gemfile.lock
+++ b/gems/gitlab-utils/Gemfile.lock
@@ -108,7 +108,7 @@ GEM
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.1)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
@@ -201,4 +201,4 @@ DEPENDENCIES
rubocop-rspec (~> 2.22)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/gems/ipynbdiff/Gemfile.lock b/gems/ipynbdiff/Gemfile.lock
index 0234a25d96a..28fe0ef299b 100644
--- a/gems/ipynbdiff/Gemfile.lock
+++ b/gems/ipynbdiff/Gemfile.lock
@@ -53,7 +53,7 @@ GEM
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.1)
- rexml (3.3.8)
+ rexml (3.3.9)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
@@ -136,4 +136,4 @@ DEPENDENCIES
simplecov (~> 0.22.0)
BUNDLED WITH
- 2.5.21
+ 2.5.22
diff --git a/lib/gitlab/background_migration/backfill_ci_runners_partitioned_table.rb b/lib/gitlab/background_migration/backfill_ci_runners_partitioned_table.rb
new file mode 100644
index 00000000000..a5069dd2068
--- /dev/null
+++ b/lib/gitlab/background_migration/backfill_ci_runners_partitioned_table.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module BackgroundMigration
+ # Background migration to copy only valid data from ci_runners to its corresponding partitioned table
+ # rubocop: disable Migration/BatchedMigrationBaseClass -- This is indirectly deriving from the correct base class
+ class BackfillCiRunnersPartitionedTable < BackfillPartitionedTable
+ extend ::Gitlab::Utils::Override
+
+ private
+
+ override :filter_sub_batch_content
+ def filter_sub_batch_content(relation)
+ relation.where(runner_type: 1).or(relation.where.not(sharding_key_id: nil))
+ end
+ end
+ # rubocop: enable Migration/BatchedMigrationBaseClass
+ end
+end
diff --git a/lib/gitlab/background_migration/backfill_partitioned_table.rb b/lib/gitlab/background_migration/backfill_partitioned_table.rb
index 6479d40a930..dbcd6b26a74 100644
--- a/lib/gitlab/background_migration/backfill_partitioned_table.rb
+++ b/lib/gitlab/background_migration/backfill_partitioned_table.rb
@@ -9,7 +9,7 @@ module Gitlab
job_arguments :partitioned_table
def perform
- validate_paritition_table!
+ validate_partition_table!
bulk_copy = Gitlab::Database::PartitioningMigrationHelpers::BulkCopy.new(
batch_table,
@@ -19,16 +19,15 @@ module Gitlab
)
each_sub_batch do |relation|
- sub_start_id, sub_stop_id = relation.pick(Arel.sql("MIN(#{batch_column}), MAX(#{batch_column})"))
- bulk_copy.copy_between(sub_start_id, sub_stop_id)
+ bulk_copy.copy_relation(filter_sub_batch_content(relation))
end
end
private
- def validate_paritition_table!
+ def validate_partition_table!
unless connection.table_exists?(partitioned_table)
- raise "exiting backfill migration because partitioned table #{partitioned_table} does not exist. " \
+ raise "exiting backfill migration because partitioned table '#{partitioned_table}' does not exist. " \
"This could be due to rollback of the migration which created the partitioned table."
end
@@ -38,6 +37,10 @@ module Gitlab
end
# rubocop: enable Style/GuardClause
end
+
+ def filter_sub_batch_content(relation)
+ relation
+ end
end
end
end
diff --git a/lib/gitlab/background_migration/backfill_root_storage_statistics_fork_storage_sizes.rb b/lib/gitlab/background_migration/backfill_root_storage_statistics_fork_storage_sizes.rb
deleted file mode 100644
index 23c510720c0..00000000000
--- a/lib/gitlab/background_migration/backfill_root_storage_statistics_fork_storage_sizes.rb
+++ /dev/null
@@ -1,99 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Backfill the following columns on the namespace_root_storage_statistics table:
- # - public_forks_storage_size
- # - internal_forks_storage_size
- # - private_forks_storage_size
- class BackfillRootStorageStatisticsForkStorageSizes < BatchedMigrationJob
- operation_name :backfill_root_storage_statistics_fork_sizes
- feature_category :consumables_cost_management
-
- VISIBILITY_LEVELS_TO_STORAGE_SIZE_COLUMNS = {
- 0 => :private_forks_storage_size,
- 10 => :internal_forks_storage_size,
- 20 => :public_forks_storage_size
- }.freeze
-
- def perform
- each_sub_batch do |sub_batch|
- sub_batch.each do |root_storage_statistics|
- next if has_fork_data?(root_storage_statistics)
-
- namespace_id = root_storage_statistics.namespace_id
-
- namespace_type = execute("SELECT type FROM namespaces WHERE id = #{namespace_id}").first&.fetch('type')
-
- next if namespace_type.nil?
-
- sql = if user_namespace?(namespace_type)
- user_namespace_sql(namespace_id)
- else
- group_namespace_sql(namespace_id)
- end
-
- stats = execute(sql)
- .map { |h| { h['projects_visibility_level'] => h['sum_project_statistics_storage_size'] } }
- .reduce({}) { |memo, h| memo.merge(h) }
- .transform_keys { |k| VISIBILITY_LEVELS_TO_STORAGE_SIZE_COLUMNS[k] }
-
- root_storage_statistics.update!(stats)
- end
- end
- end
-
- def has_fork_data?(root_storage_statistics)
- root_storage_statistics.public_forks_storage_size != 0 ||
- root_storage_statistics.internal_forks_storage_size != 0 ||
- root_storage_statistics.private_forks_storage_size != 0
- end
-
- def user_namespace?(type)
- type.nil? || type == 'User' || !(type == 'Group' || type == 'Project')
- end
-
- def execute(sql)
- ::ApplicationRecord.connection.execute(sql)
- end
-
- def user_namespace_sql(namespace_id)
- <<~SQL
- SELECT
- SUM("project_statistics"."storage_size") AS sum_project_statistics_storage_size,
- "projects"."visibility_level" AS projects_visibility_level
- FROM
- "projects"
- INNER JOIN "project_statistics" ON "project_statistics"."project_id" = "projects"."id"
- INNER JOIN "fork_network_members" ON "fork_network_members"."project_id" = "projects"."id"
- INNER JOIN "fork_networks" ON "fork_networks"."id" = "fork_network_members"."fork_network_id"
- WHERE
- "projects"."namespace_id" = #{namespace_id}
- AND (fork_networks.root_project_id != projects.id)
- GROUP BY "projects"."visibility_level"
- SQL
- end
-
- def group_namespace_sql(namespace_id)
- <<~SQL
- SELECT
- SUM("project_statistics"."storage_size") AS sum_project_statistics_storage_size,
- "projects"."visibility_level" AS projects_visibility_level
- FROM
- "projects"
- INNER JOIN "project_statistics" ON "project_statistics"."project_id" = "projects"."id"
- INNER JOIN "fork_network_members" ON "fork_network_members"."project_id" = "projects"."id"
- INNER JOIN "fork_networks" ON "fork_networks"."id" = "fork_network_members"."fork_network_id"
- WHERE
- "projects"."namespace_id" IN (
- SELECT namespaces.traversal_ids[array_length(namespaces.traversal_ids, 1)] AS id
- FROM "namespaces"
- WHERE "namespaces"."type" = 'Group' AND (traversal_ids @> ('{#{namespace_id}}'))
- )
- AND (fork_networks.root_project_id != projects.id)
- GROUP BY "projects"."visibility_level"
- SQL
- end
- end
- end
-end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
index e87707953ae..efd1e4eef9a 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb
@@ -16,7 +16,7 @@ module Gitlab
end
unless table_exists?(partitioned_table)
- logger.warn "exiting backfill migration because partitioned table #{partitioned_table} does not exist. " \
+ logger.warn "exiting backfill migration because partitioned table '#{partitioned_table}' does not exist. " \
"This could be due to the migration being rolled back after migration jobs were enqueued in sidekiq"
return
end
diff --git a/lib/gitlab/database/partitioning_migration_helpers/bulk_copy.rb b/lib/gitlab/database/partitioning_migration_helpers/bulk_copy.rb
index b8f5a2e3ad4..972c64de038 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/bulk_copy.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/bulk_copy.rb
@@ -27,6 +27,15 @@ module Gitlab
SQL
end
+ def copy_relation(relation)
+ connection.execute(<<~SQL)
+ INSERT INTO #{destination_table} (#{column_listing})
+ #{relation.select(column_listing).to_sql}
+ FOR UPDATE
+ ON CONFLICT (#{conflict_targets}) DO NOTHING
+ SQL
+ end
+
private
def column_listing
diff --git a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
index 0b800504584..19f15787159 100644
--- a/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
+++ b/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb
@@ -221,7 +221,7 @@ module Gitlab
#
# enqueue_partitioning_data_migration :audit_events
#
- def enqueue_partitioning_data_migration(table_name)
+ def enqueue_partitioning_data_migration(table_name, migration = MIGRATION)
assert_table_is_allowed(table_name)
assert_not_in_transaction_block(scope: ERROR_SCOPE)
@@ -230,7 +230,7 @@ module Gitlab
primary_key = connection.primary_key(table_name)
queue_batched_background_migration(
- MIGRATION,
+ migration,
table_name,
primary_key,
partitioned_table_name,
@@ -249,13 +249,13 @@ module Gitlab
#
# cleanup_partitioning_data_migration :audit_events
#
- def cleanup_partitioning_data_migration(table_name)
+ def cleanup_partitioning_data_migration(table_name, migration = MIGRATION)
assert_table_is_allowed(table_name)
partitioned_table_name = make_partitioned_table_name(table_name)
primary_key = connection.primary_key(table_name)
- delete_batched_background_migration(MIGRATION, table_name, primary_key, [partitioned_table_name])
+ delete_batched_background_migration(migration, table_name, primary_key, [partitioned_table_name])
end
def create_hash_partitions(table_name, number_of_partitions)
diff --git a/lib/gitlab/etag_caching/router/graphql.rb b/lib/gitlab/etag_caching/router/graphql.rb
index 50cdde88e2d..ffae222ae90 100644
--- a/lib/gitlab/etag_caching/router/graphql.rb
+++ b/lib/gitlab/etag_caching/router/graphql.rb
@@ -13,6 +13,11 @@ module Gitlab
'pipelines_graph',
'continuous_integration'
],
+ [
+ %r{\Aprojects/.+/.+/jobs\z},
+ 'jobs_table',
+ 'continuous_integration'
+ ],
[
%r(\Apipelines/sha/\w{#{Gitlab::Git::Commit::MIN_SHA_LENGTH},#{Gitlab::Git::Commit::MAX_SHA_LENGTH}}\z)o,
'ci_editor',
diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb
index fc3f7ce3ee3..45431449671 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -45,29 +45,29 @@ module Gitlab
'bg' => 0,
'cs_CZ' => 0,
'da_DK' => 21,
- 'de' => 90,
+ 'de' => 95,
'en' => 100,
'eo' => 0,
- 'es' => 25,
+ 'es' => 33,
'fil_PH' => 0,
- 'fr' => 97,
+ 'fr' => 95,
'gl_ES' => 0,
'id_ID' => 0,
- 'it' => 79,
- 'ja' => 91,
- 'ko' => 18,
+ 'it' => 83,
+ 'ja' => 92,
+ 'ko' => 20,
'nb_NO' => 15,
'nl_NL' => 0,
'pl_PL' => 2,
- 'pt_BR' => 93,
+ 'pt_BR' => 90,
'ro_RO' => 53,
'ru' => 16,
'si_LK' => 10,
'tr_TR' => 6,
'uk' => 40,
- 'zh_CN' => 96,
+ 'zh_CN' => 94,
'zh_HK' => 1,
- 'zh_TW' => 93
+ 'zh_TW' => 89
}.freeze
private_constant :TRANSLATION_LEVELS
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 824fa153c0c..6f11fed1a59 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -38814,6 +38814,9 @@ msgstr ""
msgid "PackageRegistry|%{linkStart}Wildcards%{linkEnd} such as `my-package-*` are supported."
msgstr ""
+msgid "PackageRegistry|%{message}. Delete this package and try again."
+msgstr ""
+
msgid "PackageRegistry|%{name} version %{version} was first created %{datetime}"
msgstr ""
@@ -39332,9 +39335,6 @@ msgstr ""
msgid "PackageRegistry|There was a problem fetching the details for this package."
msgstr ""
-msgid "PackageRegistry|There was a timeout and the package was not published. Delete this package and try again."
-msgstr ""
-
msgid "PackageRegistry|There was an error publishing %{count} packages"
msgstr ""
@@ -48371,6 +48371,9 @@ msgstr ""
msgid "ScanResultPolicy|Attributes are automatically applied by the scanners"
msgstr ""
+msgid "ScanResultPolicy|Automatically make approval rules optional when scan artifacts are missing from the target branch and a scan is required by a scan execution policy. This option only works with an existing scan execution policy that has matching scanners."
+msgstr ""
+
msgid "ScanResultPolicy|Block the merge request until all criteria are met"
msgstr ""
@@ -48386,6 +48389,9 @@ msgstr ""
msgid "ScanResultPolicy|Don't show me this again"
msgstr ""
+msgid "ScanResultPolicy|Edge case settings"
+msgstr ""
+
msgid "ScanResultPolicy|Except"
msgstr ""
@@ -48404,7 +48410,10 @@ msgstr ""
msgid "ScanResultPolicy|Failure cases:"
msgstr ""
-msgid "ScanResultPolicy|Fallback behavior in case of policy failure"
+msgid "ScanResultPolicy|Fallback behavior"
+msgstr ""
+
+msgid "ScanResultPolicy|Fallback behavior and edge case settings"
msgstr ""
msgid "ScanResultPolicy|False positive"
@@ -48437,6 +48446,9 @@ msgstr ""
msgid "ScanResultPolicy|License scanning allows only one criteria: Status"
msgstr ""
+msgid "ScanResultPolicy|Make approval rules optional using scan execution policies"
+msgstr ""
+
msgid "ScanResultPolicy|Matching"
msgstr ""
@@ -49765,9 +49777,6 @@ msgstr ""
msgid "SecurityOrchestration|Every time a pipeline runs for %{branches}%{branchExceptionsString}"
msgstr ""
-msgid "SecurityOrchestration|Ex: top_level_group"
-msgstr ""
-
msgid "SecurityOrchestration|Exception branches"
msgstr ""
@@ -50157,6 +50166,9 @@ msgstr ""
msgid "SecurityOrchestration|Show only linked security policy projects"
msgstr ""
+msgid "SecurityOrchestration|Something went wrong, unable to fetch groups"
+msgstr ""
+
msgid "SecurityOrchestration|Something went wrong, unable to fetch policies"
msgstr ""
@@ -65297,6 +65309,9 @@ msgstr ""
msgid "does not match dast_site_validation.project"
msgstr ""
+msgid "does not support select options."
+msgstr ""
+
msgid "download it"
msgstr ""
@@ -65377,6 +65392,9 @@ msgstr ""
msgid "exceeds the limit of %{count} links"
msgstr ""
+msgid "exceeds the limit of %{count}."
+msgstr ""
+
msgid "expired on %{timebox_due_date}"
msgstr ""
diff --git a/package.json b/package.json
index 9e2adea3037..1d7be1737c1 100644
--- a/package.json
+++ b/package.json
@@ -140,7 +140,7 @@
"colord": "^2.9.3",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
- "core-js": "^3.38.1",
+ "core-js": "^3.39.0",
"cron-validator": "^1.1.1",
"cronstrue": "^1.122.0",
"cropperjs": "^1.6.1",
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 02498d74518..5f80391255f 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -264,7 +264,7 @@ GEM
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
- rexml (3.3.8)
+ rexml (3.3.9)
rotp (6.3.0)
rspec (3.13.0)
rspec-core (~> 3.13.0)
diff --git a/qa/qa/runtime/user_store.rb b/qa/qa/runtime/user_store.rb
index a6cc526d8ab..258f9b20f14 100644
--- a/qa/qa/runtime/user_store.rb
+++ b/qa/qa/runtime/user_store.rb
@@ -23,112 +23,187 @@ module QA
return @admin_api_client if @admin_api_client
info("Creating admin api client for api fabrications")
+ @admin_api_client = create_api_client(
+ token: Env.admin_personal_access_token,
+ user_proc: -> { admin_user },
+ default_token: DEFAULT_ADMIN_API_TOKEN,
+ check_admin: true)
- if Env.admin_personal_access_token
- info("Admin api token variable is set, using it for default admin api fabrications")
- @admin_api_client = API::Client
- .new(personal_access_token: Env.admin_personal_access_token)
- .tap { |client| validate_admin_client!(client) }
- elsif default_admin_token_valid?
- info("Admin api token variable is not set, using default - '#{DEFAULT_ADMIN_API_TOKEN}'")
- @admin_api_client = API::Client.new(personal_access_token: DEFAULT_ADMIN_API_TOKEN)
- else
- @admin_api_client = create_admin_api_client(admin_user)
- end
-
- info("Admin token set up successfully")
+ info("Global admin api client set up successfully")
@admin_api_client
end
alias_method :initialize_admin_api_client, :admin_api_client
+ # TODO: Implement unique user and user api client fabrication for every spec when running on non live envs
+
+ # Global test user api client
+ # This api client is used as a primary one for resource fabrication that do not require admin priviledges
+ #
+ # @return [QA::Runtime::API::Client]
+ def user_api_client
+ return @user_api_client if defined?(@user_api_client)
+
+ info("Creating api client for runtime user")
+ @user_api_client = create_api_client(token: Env.personal_access_token, user_proc: -> { runtime_user })
+
+ info("Runtime user api client set up successfully")
+ @user_api_client
+ rescue StandardError => e
+ # consider runtime user api client optional and set to nil if not setup
+ warn("Failed to create runtime user api client: #{e.message}")
+ @user_api_client = nil
+ end
+ alias_method :initialize_user_api_client, :user_api_client
+
# Global admin user
#
# @return [QA::Resource::User]
def admin_user
return @admin_user if @admin_user
- @admin_user = Resource::User.init do |user|
- user.username = if Env.admin_username
- Env.admin_username
- else
- debug("Admin username variable not set, using default - '#{DEFAULT_ADMIN_USERNAME}'")
- DEFAULT_ADMIN_USERNAME
- end
-
- user.password = if Env.admin_password
- Env.admin_password
- else
- debug("Admin password variable not set, using default - '#{DEFAULT_ADMIN_PASSWORD}'")
- DEFAULT_ADMIN_PASSWORD
- end
- end
-
- if @admin_api_client && client_belongs_to_user?(@admin_api_client, @admin_user)
- @admin_user.api_client = @admin_api_client
- @admin_user.reload!
- elsif @admin_api_client
- warn(<<~WARN)
- Configured global admin token does not belong to configured admin user
- Please check values for GITLAB_QA_ADMIN_ACCESS_TOKEN, GITLAB_ADMIN_USERNAME and GITLAB_ADMIN_PASSWORD variables
- WARN
- end
-
- @admin_user
+ @admin_user = create_user(username: Env.admin_username, password: Env.admin_password,
+ default_username: DEFAULT_ADMIN_USERNAME, default_password: DEFAULT_ADMIN_PASSWORD,
+ api_client: @admin_api_client)
end
alias_method :initialize_admin_user, :admin_user
+ # Global test user
+ # This user is used as a primary one for test execution
+ #
+ # @return [QA::Resource::User]
+ def runtime_user
+ return @runtime_user if defined?(@runtime_user)
+
+ @runtime_user = create_user(username: Env.user_username, password: Env.user_password,
+ api_client: @user_api_client)
+ rescue StandardError => e
+ # consider runtime user optional and set to nil if not setup
+ warn("Failed to create runtime user: #{e.message}")
+ @user_api_client = nil
+ end
+ alias_method :initialize_runtime_user, :runtime_user
+
private
delegate :debug, :info, :warn, :error, to: Logger
- # Check if default admin token is present in environment and valid
+ # Create api client with provided token with fallback to UI creation of token
#
+ # @param [String] token
+ # @param [Proc] user_proc
+ # @param [String] default_token
+ # @return [QA::Runtime::API::Client]
+ def create_api_client(token:, user_proc:, default_token: nil, check_admin: false)
+ if token
+ info("Global api token variable is set, using it for api client setup")
+ API::Client
+ .new(personal_access_token: token)
+ .tap { |client| validate_api_client!(client, check_admin: check_admin) }
+ elsif token_valid?(default_token, check_admin: check_admin)
+ info("Api token variable is not set, using default - '#{default_token}'")
+ API::Client.new(personal_access_token: default_token)
+ else
+ # pass user through proc so it's lazily initialized only when fabricating token via UI
+ user = user_proc.call
+ create_api_token_via_ui!(user)
+ end
+ end
+
+ # Initialize new user
+ #
+ # @param [String] username
+ # @param [String] password
+ # @param [String] default_username
+ # @param [String] default_password
+ # @param [QA::Runtime::API::Client] api_client
+ # @return [QA::Resource::User]
+ def create_user(username:, password:, default_username: nil, default_password: nil, api_client: nil)
+ return if (username.nil? && default_username.nil?) || (password.nil? && default_password.nil?)
+
+ user = Resource::User.init do |user|
+ user.username = if username
+ username
+ else
+ debug("Username variable not set, using default - '#{default_username}'")
+ default_username
+ end
+
+ user.password = if password
+ password
+ else
+ debug("Password variable not set, using default - '#{default_password}'")
+ default_password
+ end
+ end
+
+ if api_client && client_belongs_to_user?(api_client, user)
+ user.api_client = api_client
+ user.reload!
+ elsif api_client
+ warn(<<~WARN)
+ Configured global api client does not belong to configured global user
+ Please check values for user authentication related variables
+ WARN
+ end
+
+ user
+ end
+
+ # Check if provided token is valid?
+ #
+ # @param [String] token
+ # @param [Boolean] check_admin
# @return [Boolean]
- def default_admin_token_valid?
- debug("Validating presence of default admin api token in environment")
- validate_admin_client!(API::Client.new(personal_access_token: DEFAULT_ADMIN_API_TOKEN))
- debug("Default admin token is present in environment and is valid")
+ def token_valid?(token, check_admin:)
+ return unless token
+
+ debug("Validating if api token is valid")
+ validate_api_client!(API::Client.new(personal_access_token: token), check_admin: check_admin)
+ debug("Api token is valid")
true
rescue InvalidTokenError
- debug("Default admin token is not valid or present in environment, skipping...")
+ debug("Api token is not valid, skipping...")
false
end
- # Create admin access client and validate it
+ # Create api token via UI for provided user
+ # Update user api_client to use fabricated token
#
# @param [QA::Resource::User] user
# @return [QA::Runtime::API::Client]
- def create_admin_api_client(user)
- info("Creating admin token via ui")
- admin_token = Flow::Login.while_signed_in(as: user) do
- Resource::PersonalAccessToken.fabricate_via_browser_ui! { |pat| pat.user = user }.token
+ def create_api_token_via_ui!(user)
+ info("Creating personal access token via UI for user #{user.username}")
+ pat = Flow::Login.while_signed_in(as: user) do
+ Resource::PersonalAccessToken.fabricate_via_browser_ui! { |pat| pat.user = user }
end
- API::Client.new(:gitlab, personal_access_token: admin_token).tap do |client|
- validate_admin_client!(client)
- user.api_client = client
- user.reload!
- end
+ api_client = Runtime::API::Client.new(personal_access_token: pat.token)
+ user.api_client = api_client
+ user.reload!
+
+ api_client
end
# Validate if client belongs to an admin user
#
# @param [QA::Runtime::API::Client] client
# @return [void]
- def validate_admin_client!(client)
- debug("Validating admin access token")
+ def validate_api_client!(client, check_admin: true)
+ debug("Validating api client")
resp = fetch_user_details(client)
if resp.code == 403 && resp.body.include?("Your password expired")
- raise ExpiredAdminPasswordError, "Admin password has expired and must be reset"
+ raise ExpiredAdminPasswordError, "Password for client's user has expired and must be reset"
elsif !status_ok?(resp)
- raise InvalidTokenError, "Admin token validation failed! Code: #{resp.code}, Err: '#{resp.body}'"
+ raise InvalidTokenError, "API client validation failed! Code: #{resp.code}, Err: '#{resp.body}'"
end
- is_admin = Support::API.parse_body(resp)[:is_admin]
- raise InvalidTokenError, "Admin token does not belong to admin user" unless is_admin
+ if check_admin
+ is_admin = Support::API.parse_body(resp)[:is_admin]
+ raise InvalidTokenError, "Admin token does not belong to admin user" unless is_admin
+ end
- debug("Admin token is valid")
+ debug("API client is valid")
end
# Check if token belongs to specific user
@@ -139,7 +214,7 @@ module QA
def client_belongs_to_user?(client, user)
resp = fetch_user_details(client)
unless status_ok?(resp)
- raise InvalidTokenError, "Token validation failed! Code: #{resp.code}, Err: '#{resp.body}'"
+ raise InvalidTokenError, "API client validation failed! Code: #{resp.code}, Err: '#{resp.body}'"
end
Support::API.parse_body(resp)[:username] == user.username
diff --git a/qa/spec/runtime/user_store_spec.rb b/qa/spec/runtime/user_store_spec.rb
index 8aea016581e..29e06d6e1ab 100644
--- a/qa/spec/runtime/user_store_spec.rb
+++ b/qa/spec/runtime/user_store_spec.rb
@@ -12,14 +12,17 @@ module QA
warn: nil,
error: nil
})
- allow(Runtime::Env).to receive_messages({
- admin_username: nil,
- admin_password: nil,
- admin_personal_access_token: nil
- })
described_class.instance_variable_set(:@admin_api_client, nil)
described_class.instance_variable_set(:@admin_user, nil)
+
+ if described_class.instance_variable_defined?(:@user_api_client)
+ described_class.send(:remove_instance_variable, :@user_api_client)
+ end
+
+ if described_class.instance_variable_defined?(:@runtime_user)
+ described_class.send(:remove_instance_variable, :@runtime_user)
+ end
end
def mock_user_get(token:, code: 200, body: { is_admin: true, id: 1, username: "root" }.to_json)
@@ -33,6 +36,11 @@ module QA
before do
allow(Runtime::Env).to receive(:admin_personal_access_token).and_return(admin_token)
+ allow(Runtime::Env).to receive_messages({
+ admin_username: nil,
+ admin_password: nil,
+ admin_personal_access_token: admin_token
+ })
end
context "when admin token variable is set" do
@@ -66,7 +74,7 @@ module QA
it "raises InvalidTokenError" do
expect { described_class.admin_api_client }.to raise_error(
- described_class::InvalidTokenError, "Admin token validation failed! Code: 401, Err: '401 Unauthorized'"
+ described_class::InvalidTokenError, "API client validation failed! Code: 401, Err: '401 Unauthorized'"
)
end
end
@@ -80,35 +88,43 @@ module QA
it "raises ExpiredAdminPasswordError" do
expect { described_class.admin_api_client }.to raise_error(
- described_class::ExpiredAdminPasswordError, "Admin password has expired and must be reset"
+ described_class::ExpiredAdminPasswordError, "Password for client's user has expired and must be reset"
)
end
end
context "with token creation via UI" do
- let(:admin_user) { Resource::User.new }
- let(:pat) { Resource::PersonalAccessToken.init { |pat| pat.token = "test" } }
+ let(:token) { "token" }
+
+ # dummy objects are created with populated id fields to simulate proper fabrication and reload calls
+ let(:admin_user) { Resource::User.init { |u| u.id = 1 } }
+ let(:pat) { Resource::PersonalAccessToken.init { |p| p.token = token } }
before do
+ allow(Flow::Login).to receive(:while_signed_in).with(as: admin_user).and_yield
allow(Resource::User).to receive(:init).and_yield(admin_user).and_return(admin_user)
allow(Resource::PersonalAccessToken).to receive(:fabricate_via_browser_ui!).and_yield(pat).and_return(pat)
- allow(Flow::Login).to receive(:while_signed_in).with(as: admin_user).and_yield
allow(admin_user).to receive(:reload!)
mock_user_get(token: default_admin_token, code: 401)
- mock_user_get(token: pat.token)
end
it "creates admin api client with token created from UI" do
- expect(described_class.admin_api_client.personal_access_token).to eq(pat.token)
- expect(admin_user.username).to eq("root")
- expect(admin_user.password).to eq("5iveL!fe")
+ expect(described_class.admin_api_client.personal_access_token).to eq(token)
expect(admin_user).to have_received(:reload!)
end
end
end
describe "#admin_user" do
+ before do
+ allow(Runtime::Env).to receive_messages({
+ admin_username: nil,
+ admin_password: nil,
+ admin_personal_access_token: nil
+ })
+ end
+
context "when admin client has not been initialized" do
context "with admin user variables set" do
let(:username) { "admin-username" }
@@ -166,8 +182,8 @@ module QA
described_class.initialize_admin_user
expect(Runtime::Logger).to have_received(:warn).with(<<~WARN)
- Configured global admin token does not belong to configured admin user
- Please check values for GITLAB_QA_ADMIN_ACCESS_TOKEN, GITLAB_ADMIN_USERNAME and GITLAB_ADMIN_PASSWORD variables
+ Configured global api client does not belong to configured global user
+ Please check values for user authentication related variables
WARN
end
end
@@ -179,7 +195,182 @@ module QA
it "raises invalid token error" do
expect { described_class.admin_user }.to raise_error(
- described_class::InvalidTokenError, "Token validation failed! Code: 403, Err: 'Unauthorized'"
+ described_class::InvalidTokenError, "API client validation failed! Code: 403, Err: 'Unauthorized'"
+ )
+ end
+ end
+ end
+ end
+
+ describe "#user_api_client" do
+ subject(:user_api_client) { described_class.user_api_client }
+
+ let(:username) { "username" }
+ let(:password) { "password" }
+ let(:api_token) { "token" }
+
+ before do
+ allow(Runtime::Env).to receive_messages({
+ user_username: username,
+ user_password: password,
+ personal_access_token: api_token
+ })
+ end
+
+ context "when api token variable is set" do
+ before do
+ mock_user_get(token: api_token)
+ end
+
+ it "creates admin api client with configured token" do
+ expect(user_api_client.personal_access_token).to eq(api_token)
+ end
+ end
+
+ context "when api token variable and user variables are not set" do
+ let(:api_token) { nil }
+ let(:username) { nil }
+ let(:password) { nil }
+
+ it "does not return api client" do
+ expect(user_api_client).to be_nil
+ end
+ end
+
+ context "with invalid token set via environment variable" do
+ before do
+ mock_user_get(token: api_token, code: 401, body: "401 Unauthorized")
+ end
+
+ it "does not return api client" do
+ expect(user_api_client).to be_nil
+ end
+ end
+
+ context "with expired admin password" do
+ before do
+ mock_user_get(token: api_token, code: 403, body: "Your password expired")
+ end
+
+ it "does not return api client" do
+ expect(user_api_client).to be_nil
+ end
+ end
+
+ context "with token creation via UI" do
+ let(:api_token) { nil }
+ # dummy objects are created with populated id fields to simulate proper fabrication and reload calls
+ let(:user_spy) { Resource::User.init { |u| u.id = 1 } }
+ let(:pat) { Resource::PersonalAccessToken.init { |p| p.token = "token" } }
+
+ before do
+ allow(Flow::Login).to receive(:while_signed_in).with(as: user_spy).and_yield
+ allow(Resource::User).to receive(:init).and_yield(user_spy).and_return(user_spy)
+ allow(Resource::PersonalAccessToken).to receive(:fabricate_via_browser_ui!).and_yield(pat).and_return(pat)
+ allow(user_spy).to receive(:reload!)
+ end
+
+ it "creates user api client with token created from UI" do
+ expect(user_api_client.personal_access_token).to eq(pat.token)
+ expect(user_spy).to have_received(:reload!)
+ end
+ end
+ end
+
+ describe "#runtime_user" do
+ subject(:runtime_user) { described_class.runtime_user }
+
+ let(:username) { "username" }
+ let(:password) { "password" }
+
+ before do
+ allow(Runtime::Env).to receive_messages({
+ user_username: username,
+ user_password: password,
+ personal_access_token: nil
+ })
+ end
+
+ context "when api client has not been initialized" do
+ context "with user variables set" do
+ it "returns user with configured credentials" do
+ expect(runtime_user.username).to eq(username)
+ expect(runtime_user.password).to eq(password)
+ end
+ end
+
+ context "without user variables set" do
+ let(:username) { nil }
+ let(:password) { nil }
+
+ it "does not return runtime user" do
+ expect(runtime_user).to be_nil
+ end
+ end
+
+ context "with only username set" do
+ let(:password) { nil }
+
+ it "does not return runtime user" do
+ expect(runtime_user).to be_nil
+ end
+ end
+
+ context "with only password set" do
+ let(:username) { nil }
+
+ it "does not return runtime user" do
+ expect(runtime_user).to be_nil
+ end
+ end
+ end
+
+ context "when api client has been initialized" do
+ let(:user_spy) { Resource::User.new }
+ let(:api_client) { Runtime::API::Client.new(personal_access_token: "token") }
+
+ before do
+ allow(Resource::User).to receive(:init).and_yield(user_spy).and_return(user_spy)
+ allow(user_spy).to receive(:reload!)
+
+ described_class.instance_variable_set(:@user_api_client, api_client)
+ end
+
+ context "with valid client belonging to user" do
+ before do
+ mock_user_get(token: api_client.personal_access_token, body: { username: username }.to_json)
+ end
+
+ it "sets api client on user and reloads it" do
+ expect(runtime_user.instance_variable_get(:@api_client)).to eq(api_client)
+ expect(runtime_user).to have_received(:reload!)
+ end
+ end
+
+ context "with valid client not belonging to user" do
+ before do
+ mock_user_get(token: api_client.personal_access_token, body: { username: "test" }.to_json)
+ end
+
+ it "prints warning message" do
+ described_class.initialize_runtime_user
+
+ expect(Runtime::Logger).to have_received(:warn).with(<<~WARN)
+ Configured global api client does not belong to configured global user
+ Please check values for user authentication related variables
+ WARN
+ end
+ end
+
+ context "with invalid api client" do
+ before do
+ mock_user_get(token: api_client.personal_access_token, code: 403, body: "Unauthorized")
+ end
+
+ it "raises invalid token error" do
+ expect(runtime_user).to be_nil
+ expect(Runtime::Logger).to have_received(:warn).with(
+ "Failed to create runtime user: API client validation failed! Code: 403, Err: 'Unauthorized'"
)
end
end
diff --git a/spec/features/merge_requests/user_filters_by_milestones_spec.rb b/spec/features/merge_requests/user_filters_by_milestones_spec.rb
index 3bbf77b3617..2ce1ee15637 100644
--- a/spec/features/merge_requests/user_filters_by_milestones_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_milestones_spec.rb
@@ -18,22 +18,32 @@ RSpec.describe 'Merge Requests > User filters by milestones', :js, feature_categ
end
it 'filters by no milestone' do
- select_tokens 'Milestone', 'None', submit: true
+ select_tokens 'Milestone', '=', 'None', submit: true
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
it 'filters by a specific milestone' do
- select_tokens 'Milestone', milestone.title, submit: true
+ select_tokens 'Milestone', '=', milestone.title, submit: true
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
+ it 'filters out a specific milestone' do
+ select_tokens 'Milestone', '!=', milestone.title, submit: true
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).to have_css('.merge-request', count: 1)
+ page.within('.issuable-list') do
+ expect(page).not_to have_text(milestone.title)
+ end
+ end
+
describe 'filters by upcoming milestone' do
it 'does not show merge requests with no expiry' do
- select_tokens 'Milestone', 'Upcoming', submit: true
+ select_tokens 'Milestone', '=', 'Upcoming', submit: true
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).to have_css('.merge-request', count: 0)
@@ -43,7 +53,7 @@ RSpec.describe 'Merge Requests > User filters by milestones', :js, feature_categ
let(:milestone) { create(:milestone, project: project, due_date: Date.tomorrow) }
it 'shows merge requests' do
- select_tokens 'Milestone', 'Upcoming', submit: true
+ select_tokens 'Milestone', '=', 'Upcoming', submit: true
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
@@ -54,7 +64,7 @@ RSpec.describe 'Merge Requests > User filters by milestones', :js, feature_categ
let(:milestone) { create(:milestone, project: project, due_date: Date.yesterday) }
it 'does not show any merge requests' do
- select_tokens 'Milestone', 'Upcoming', submit: true
+ select_tokens 'Milestone', '=', 'Upcoming', submit: true
expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
expect(page).to have_css('.merge-request', count: 0)
diff --git a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
index 54eef3370cf..67c980e7c1a 100644
--- a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Merge requests > User filters by multiple criteria', :js, featur
describe 'filtering by text, author, assignee, milestone, and label' do
it 'filters by text, author, assignee, milestone, and label' do
- select_tokens 'Author', '=', user.username, 'Assignee', '=', user.username, 'Milestone', milestone.title, 'Label', '=', wontfix.title
+ select_tokens 'Author', '=', user.username, 'Assignee', '=', user.username, 'Milestone', '=', milestone.title, 'Label', '=', wontfix.title
send_keys 'Bug', :enter, :enter
expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
index c20ba70d7d1..60160eb794b 100644
--- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -9,7 +9,7 @@ exports[`Confidential merge request project form group component renders empty s
No forks are available to you.
@@ -56,7 +56,7 @@ exports[`Confidential merge request project form group component renders fork dr
selectedproject="[object Object]"
/>
To protect this issue's confidentiality, a private fork of this project was selected.
Open source software to collaborate on code
diff --git a/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js b/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js
index 1af59ad9a19..a9f6f2570ff 100644
--- a/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js
+++ b/spec/frontend/merge_requests/list/components/merge_requests_list_app_spec.js
@@ -421,6 +421,10 @@ describe('Merge requests list app', () => {
type: 'source-branch',
value: { data: ['branch_name'], operator: OPERATOR_NOT },
},
+ {
+ type: 'target-branch',
+ value: { data: ['branch_name'], operator: OPERATOR_NOT },
+ },
]);
await nextTick();
@@ -431,6 +435,7 @@ describe('Merge requests list app', () => {
assigneeUsernames: ['root'],
reviewerUsername: 'root',
sourceBranches: ['branch_name'],
+ targetBranches: ['branch_name'],
},
}),
);
@@ -441,6 +446,7 @@ describe('Merge requests list app', () => {
assigneeUsernames: ['root'],
reviewerUsername: 'root',
sourceBranches: ['branch_name'],
+ targetBranches: ['branch_name'],
},
}),
);
@@ -462,6 +468,10 @@ describe('Merge requests list app', () => {
type: 'source-branch',
value: { data: ['branch_name'], operator: OPERATOR_NOT },
},
+ {
+ type: 'target-branch',
+ value: { data: ['branch_name'], operator: OPERATOR_NOT },
+ },
]);
await nextTick();
@@ -471,6 +481,7 @@ describe('Merge requests list app', () => {
'not[assignee_username][]': ['root'],
'not[reviewer_username]': 'root',
'not[source_branches][]': ['branch_name'],
+ 'not[target_branches][]': ['branch_name'],
}),
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_errors_count_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_errors_count_spec.js
index 1b10bb00f12..2e3fa17845c 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_errors_count_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_errors_count_spec.js
@@ -1,22 +1,46 @@
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import { GlAlert, GlButton } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
+import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import PackageErrorsCount from '~/packages_and_registries/package_registry/components/list/package_errors_count.vue';
-import { packageData } from '../../mock_data';
+import getPackageErrorsCountQuery from '~/packages_and_registries/package_registry/graphql/queries/get_package_errors_count.query.graphql';
+import { errorPackagesListQuery } from '../../mock_data';
+
+Vue.use(VueApollo);
describe('PackageErrorsCount', () => {
+ let apolloProvider;
let wrapper;
- const firstPackage = packageData();
- const errorPackage = {
- ...packageData(),
- id: 'gid://gitlab/Packages::Package/121',
- status: 'ERROR',
- name: 'error package',
+ const mockFullPath = 'test-group/test-project';
+
+ const firstPackage = {
+ __typename: 'Package',
+ id: 'gid://gitlab/Packages::Package/1',
+ name: '@gitlab-org/package-15',
+ statusMessage: 'custom error message',
+ version: '1.0.0',
+ };
+
+ const secondPackage = {
+ __typename: 'Package',
+ id: 'gid://gitlab/Packages::Package/2',
+ name: '@gitlab-org/package-16',
+ statusMessage: null,
+ version: '2.0.0',
+ };
+
+ const defaultProvide = {
+ isGroupPage: true,
+ fullPath: mockFullPath,
};
const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
@@ -25,10 +49,21 @@ describe('PackageErrorsCount', () => {
const showMock = jest.fn();
- const mountComponent = ({ props = {}, stubs = {} } = {}) => {
+ const mountComponent = ({
+ provide = {},
+ resolver = jest
+ .fn()
+ .mockResolvedValue(errorPackagesListQuery({ extend: { count: 1, nodes: [firstPackage] } })),
+ stubs = {},
+ } = {}) => {
+ const requestHandlers = [[getPackageErrorsCountQuery, resolver]];
+ apolloProvider = createMockApollo(requestHandlers);
+
wrapper = shallowMountExtended(PackageErrorsCount, {
- propsData: {
- ...props,
+ apolloProvider,
+ provide: {
+ ...defaultProvide,
+ ...provide,
},
stubs: {
DeleteModal: stubComponent(DeleteModal, {
@@ -41,41 +76,114 @@ describe('PackageErrorsCount', () => {
});
};
- describe('when an error package is present', () => {
- beforeEach(() => {
- mountComponent({ props: { errorPackages: [errorPackage] } });
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ });
+
+ describe.each`
+ description | resolver | isError
+ ${'empty response'} | ${jest.fn().mockResolvedValue(errorPackagesListQuery({ extend: { nodes: [], count: 0 } }))} | ${false}
+ ${'error response'} | ${jest.fn().mockResolvedValue({ data: { group: null } })} | ${true}
+ ${'unhandled exception response'} | ${jest.fn().mockRejectedValue(new Error('error'))} | ${true}
+ `(`with $description`, ({ resolver, isError }) => {
+ beforeEach(async () => {
+ mountComponent({ resolver });
+
+ await waitForPromises();
});
- it('should display an alert with default body message', () => {
- expect(findErrorPackageAlert().exists()).toBe(true);
- expect(findErrorPackageAlert().props('title')).toBe(
- 'There was an error publishing error package',
- );
- expect(findErrorPackageAlert().text()).toBe(
- 'There was a timeout and the package was not published. Delete this package and try again.',
- );
+ it('does not show alert', () => {
+ expect(findErrorPackageAlert().exists()).toBe(false);
});
- it('should display alert body with message set in `statusMessage`', () => {
+ if (isError) {
+ it('captures error in Sentry', () => {
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+ }
+ });
+
+ describe.each`
+ type
+ ${WORKSPACE_PROJECT}
+ ${WORKSPACE_GROUP}
+ `('$type query', ({ type }) => {
+ let provide;
+ let resolver;
+
+ const isGroupPage = type === WORKSPACE_GROUP;
+
+ beforeEach(async () => {
+ provide = { ...defaultProvide, isGroupPage };
+ resolver = jest
+ .fn()
+ .mockResolvedValue(errorPackagesListQuery({ type, extend: { nodes: [], count: 0 } }));
+
mountComponent({
- props: {
- errorPackages: [{ ...errorPackage, statusMessage: 'custom error message' }],
- },
+ provide,
+ resolver,
+ });
+ await waitForPromises();
+ });
+
+ it('calls the resolver with the right parameters', () => {
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ isGroupPage, fullPath: defaultProvide.fullPath }),
+ );
+ });
+
+ it('expects not to call sentry', () => {
+ expect(Sentry.captureException).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when an error package is present', () => {
+ beforeEach(async () => {
+ mountComponent();
+ await waitForPromises();
+ });
+
+ it('should display an alert with the package name in the title', () => {
+ expect(findErrorPackageAlert().props('title')).toBe(
+ 'There was an error publishing @gitlab-org/package-15',
+ );
+ });
+
+ describe('when statusMessage is returned', () => {
+ it('should display the statusMessage in the alert body', () => {
+ expect(findErrorPackageAlert().text()).toBe(
+ 'custom error message. Delete this package and try again.',
+ );
+ });
+ });
+
+ describe('when no statusMessage is returned', () => {
+ beforeEach(async () => {
+ const withoutStatusMessage = errorPackagesListQuery({
+ extend: {
+ count: 1,
+ nodes: [secondPackage],
+ },
+ });
+ mountComponent({
+ resolver: jest.fn().mockResolvedValue(withoutStatusMessage),
+ });
+ await waitForPromises();
});
- expect(findErrorPackageAlert().exists()).toBe(true);
- expect(findErrorPackageAlert().props('title')).toBe(
- 'There was an error publishing error package',
- );
- expect(findErrorPackageAlert().text()).toBe('custom error message');
+ it('should display the generic package error in the alert body', () => {
+ expect(findErrorPackageAlert().text()).toBe(
+ 'Invalid Package: failed metadata extraction. Delete this package and try again.',
+ );
+ });
});
describe('`Delete this package` button', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mountComponent({
- props: { errorPackages: [errorPackage] },
stubs: { GlAlert },
});
+ await waitForPromises();
});
it('displays the button within the alert', () => {
@@ -96,7 +204,7 @@ describe('PackageErrorsCount', () => {
expect(showMock).toHaveBeenCalledTimes(1);
- expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([errorPackage]);
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual([firstPackage]);
});
describe('when modal confirms', () => {
@@ -106,44 +214,54 @@ describe('PackageErrorsCount', () => {
});
it('emits delete when modal confirms', () => {
- expect(wrapper.emitted('confirm-delete')[0][0]).toEqual([errorPackage]);
+ expect(wrapper.emitted('confirm-delete')[0][0]).toEqual([firstPackage]);
});
});
});
});
describe('when multiple error packages are present', () => {
- beforeEach(() => {
- mountComponent({
- props: { errorPackages: [{ ...firstPackage, status: errorPackage.status }, errorPackage] },
+ const multipleErrorPackages = errorPackagesListQuery({
+ extend: {
+ count: 2,
+ nodes: [firstPackage, secondPackage],
+ },
+ });
+
+ describe('should display an alert', () => {
+ beforeEach(async () => {
+ mountComponent({
+ resolver: jest.fn().mockResolvedValue(multipleErrorPackages),
+ });
+ await waitForPromises();
+ });
+
+ it('with count of packages in the title', () => {
+ expect(findErrorPackageAlert().props('title')).toBe(
+ 'There was an error publishing 2 packages',
+ );
+ });
+
+ it('with count of packages in the body', () => {
+ expect(findErrorPackageAlert().text()).toBe(
+ 'Failed to publish 2 packages. Delete these packages and try again.',
+ );
});
});
- it('should display an alert with default body message', () => {
- expect(findErrorPackageAlert().props('title')).toBe(
- 'There was an error publishing 2 packages',
- );
- expect(findErrorPackageAlert().text()).toBe(
- 'Failed to publish 2 packages. Delete these packages and try again.',
- );
- });
-
describe('`Show packages with errors` button', () => {
- beforeEach(() => {
+ beforeEach(async () => {
setWindowLocation(`${TEST_HOST}/foo?type=maven&after=1234`);
mountComponent({
- props: {
- errorPackages: [{ ...firstPackage, status: errorPackage.status }, errorPackage],
- },
+ resolver: jest.fn().mockResolvedValue(multipleErrorPackages),
stubs: { GlAlert },
});
+ await waitForPromises();
});
it('is shown with correct href within the alert', () => {
expect(findErrorAlertButton().text()).toBe('Show packages with errors');
- expect(findErrorAlertButton().attributes('href')).toBe(
- `${TEST_HOST}/foo?type=maven&status=error`,
- );
+ expect(findErrorAlertButton().attributes('href')).toBe(`${TEST_HOST}/foo?status=error`);
});
it('has tracking attributes', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 12e7076777e..675e6aa5bdc 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -471,3 +471,16 @@ export const packagesListQuery = ({ type = 'group', extend = {}, extendPaginatio
},
},
});
+
+export const errorPackagesListQuery = ({ type = 'group', extend = {} } = {}) => ({
+ data: {
+ [type]: {
+ id: '1',
+ packages: {
+ ...extend,
+ __typename: 'PackageConnection',
+ },
+ __typename: capitalize(type),
+ },
+ },
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index a826b0681b6..a73f796245c 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -232,32 +232,10 @@ describe('PackagesListApp', () => {
});
it('renders PackageErrorsCount component', async () => {
- const packagesList = packagesListQuery();
- const [firstPackage] = packagesList.data.group.packages.nodes;
-
- const errorPackage = {
- ...firstPackage,
- status: 'ERROR',
- };
- mountComponent({
- resolver: jest.fn().mockResolvedValue(
- packagesListQuery({
- extend: {
- packages: {
- count: 1,
- nodes: [errorPackage],
- pageInfo: {},
- },
- },
- }),
- ),
- });
findSearch().vm.$emit('update', searchPayload);
await waitForPromises();
- expect(findPackageErrorsCount().props('errorPackages')).toStrictEqual(
- expect.arrayContaining([expect.objectContaining({ id: packageData().id })]),
- );
+ expect(findPackageErrorsCount().exists()).toBe(true);
});
describe('when packageStatus filter is set to error', () => {
@@ -470,30 +448,11 @@ describe('PackagesListApp', () => {
});
it('deletePackages is bound to package-errors-count delete event', async () => {
- const packagesList = packagesListQuery();
- const [firstPackage] = packagesList.data.group.packages.nodes;
-
- const errorPackage = {
- ...firstPackage,
- status: 'ERROR',
- };
- mountComponent({
- resolver: jest.fn().mockResolvedValue(
- packagesListQuery({
- extend: {
- packages: {
- count: 1,
- nodes: [errorPackage],
- pageInfo: {},
- },
- },
- }),
- ),
- });
+ mountComponent();
await waitForFirstRequest();
- findPackageErrorsCount().vm.$emit('confirm-delete', [errorPackage]);
+ findPackageErrorsCount().vm.$emit('confirm-delete', [{ id: 1 }]);
expect(findDeletePackages().emitted('start')).toHaveLength(1);
});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 2b52c0d375b..38fdd055de1 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -38,12 +38,12 @@ exports[`Repository table row component renders a symlink table row 1`] = `
class="cursor-default gl-hidden sm:gl-table-cell tree-commit"
>
|
|
|
{
+ expect(batched_migration).not_to have_scheduled_batched_migration
+ }
+
+ migration.after -> {
+ expect(batched_migration).to have_scheduled_batched_migration(
+ table_name: :ci_runners,
+ column_name: :id,
+ interval: described_class::BATCH_INTERVAL,
+ batch_size: described_class::BATCH_SIZE,
+ sub_batch_size: described_class::SUB_BATCH_SIZE,
+ job_arguments: ['ci_runners_e59bb2812d']
+ )
+ }
+ end
+ end
+end
diff --git a/spec/scripts/internal_events/server_spec.rb b/spec/scripts/internal_events/server_spec.rb
index b3b1693591a..87c63f5da03 100644
--- a/spec/scripts/internal_events/server_spec.rb
+++ b/spec/scripts/internal_events/server_spec.rb
@@ -138,7 +138,7 @@ RSpec.describe Server, feature_category: :service_ping do
]
end
- it 'successfully parses event' do
+ it 'successfully parses event', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/498772' do
expect(response.code).to eq('200')
expect(events).to match_array(expected_events)
end
diff --git a/spec/support/helpers/user_with_namespace_shim.yml b/spec/support/helpers/user_with_namespace_shim.yml
index 1d75e754a93..37f5b455c74 100644
--- a/spec/support/helpers/user_with_namespace_shim.yml
+++ b/spec/support/helpers/user_with_namespace_shim.yml
@@ -649,7 +649,6 @@
- spec/lib/feature_spec.rb
- spec/lib/generators/gitlab/partitioning/foreign_keys_generator_spec.rb
- spec/lib/gitlab/analytics/usage_trends/workers_argument_builder_spec.rb
-- spec/lib/gitlab/background_migration/backfill_root_storage_statistics_fork_storage_sizes_spec.rb
- spec/lib/gitlab/background_migration/backfill_user_details_fields_spec.rb
- spec/lib/gitlab/background_migration/job_coordinator_spec.rb
- spec/lib/gitlab/checks/container_moved_spec.rb
diff --git a/vendor/gems/sidekiq-reliable-fetch/Gemfile.lock b/vendor/gems/sidekiq-reliable-fetch/Gemfile.lock
index 484370fdfcc..5d3afe0d225 100644
--- a/vendor/gems/sidekiq-reliable-fetch/Gemfile.lock
+++ b/vendor/gems/sidekiq-reliable-fetch/Gemfile.lock
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
- gitlab-sidekiq-fetcher (0.11.0)
+ gitlab-sidekiq-fetcher (0.12.0)
json (>= 2.5)
sidekiq (~> 7.0)
diff --git a/vendor/gems/sidekiq-reliable-fetch/README.md b/vendor/gems/sidekiq-reliable-fetch/README.md
index 5e218a76cd5..68ae9b7a1d3 100644
--- a/vendor/gems/sidekiq-reliable-fetch/README.md
+++ b/vendor/gems/sidekiq-reliable-fetch/README.md
@@ -16,15 +16,40 @@ However, it comes at a cost because `rpoplpush` can't watch multiple lists at th
### Interruption handling
-Sidekiq expects any job to report succcess or to fail. In the last case, Sidekiq puts `retry_count` counter
-into the job and keeps to re-run the job until the counter reched the maximum allowed value. When the job has
-not been given a chance to finish its work(to report success or fail), for example, when it was killed forcibly or when the job was requeued, after receiving TERM signal, the standard retry mechanisme does not get into the game and the job will be retried indefinatelly. This is why Reliable fetcher maintains a special counter `interrupted_count`
+Sidekiq expects any job to report success or to fail. In the last case, Sidekiq puts `retry_count` counter
+into the job and keeps to re-run the job until the counter reached the maximum allowed value. When the job has
+not been given a chance to finish its work(to report success or fail), for example, when it was killed forcibly or when the job was requeued, after receiving TERM signal, the standard retry mechanism does not get into the game and the job will be retried indefinitely. This is why Reliable fetcher maintains a special counter `interrupted_count`
which is used to limit the amount of such retries. In both cases, Reliable Fetcher increments counter `interrupted_count` and rejects the job from running again when the counter exceeds `max_retries_after_interruption` times (default: 3 times).
Such a job will be put to `interrupted` queue. This queue mostly behaves as Sidekiq Dead queue so it only stores a limited amount of jobs for a limited term. Same as for Dead queue, all the limits are configurable via `interrupted_max_jobs` (default: 10_000) and `interrupted_timeout_in_seconds` (default: 3 months) Sidekiq option keys.
You can also disable special handling of interrupted jobs by setting `max_retries_after_interruption` into `-1`.
In this case, interrupted jobs will be run without any limits from Reliable Fetcher and they won't be put into Interrupted queue.
+You can define the `sidekiq_interruptions_exhausted` block to execute specific actions when a job is sent to the
+`interrupted` queue after reaching the maximum allowed interruptions. For example, you might notify a user that the
+job was interrupted multiple times and will no longer be retried.
+
+The block receives a hash containing useful job details, including:
+
+- `job['class']`: The worker class name.
+- `job['args']`: Arguments passed to the job when it was enqueued.
+- `job['jid']`: The unique job ID.
+- `job['retry_count']`: The number of retry attempts made.
+- `job['interrupted_count']`: The total number of times the job was interrupted.
+
+#### Example Usage
+
+```ruby
+class MyWorker
+ include Sidekiq::Worker
+ include Sidekiq::InterruptionsExhausted
+
+ sidekiq_interruptions_exhausted do |job|
+ # Add your custom handling code here, for example:
+ notify_user("Job #{job['class']} with ID #{job['jid']} was interrupted #{job['interrupted_count']} times and will no longer be retried.")
+ end
+end
+```
## Installation
diff --git a/vendor/gems/sidekiq-reliable-fetch/gitlab-sidekiq-fetcher.gemspec b/vendor/gems/sidekiq-reliable-fetch/gitlab-sidekiq-fetcher.gemspec
index df89abca4ac..e3fdb0cdec9 100644
--- a/vendor/gems/sidekiq-reliable-fetch/gitlab-sidekiq-fetcher.gemspec
+++ b/vendor/gems/sidekiq-reliable-fetch/gitlab-sidekiq-fetcher.gemspec
@@ -1,6 +1,6 @@
Gem::Specification.new do |s|
s.name = 'gitlab-sidekiq-fetcher'
- s.version = '0.11.0'
+ s.version = '0.12.0'
s.authors = ['TEA', 'GitLab']
s.email = 'valery@gitlab.com'
s.license = 'LGPL-3.0'
diff --git a/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq-reliable-fetch.rb b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq-reliable-fetch.rb
index df44fabaedd..572888fe13b 100644
--- a/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq-reliable-fetch.rb
+++ b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq-reliable-fetch.rb
@@ -4,3 +4,4 @@ require 'sidekiq/api'
require_relative 'sidekiq/base_reliable_fetch'
require_relative 'sidekiq/reliable_fetch'
require_relative 'sidekiq/semi_reliable_fetch'
+require_relative 'sidekiq/interruptions_exhausted'
diff --git a/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/base_reliable_fetch.rb b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/base_reliable_fetch.rb
index 68268dc6ff4..e7c4f62d5cd 100644
--- a/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/base_reliable_fetch.rb
+++ b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/base_reliable_fetch.rb
@@ -247,6 +247,23 @@ module Sidekiq
message: %(Reliable Fetcher: adding dead #{msg['class']} job #{msg['jid']} to interrupted queue)
)
+ begin
+ job_class = Object.const_get(msg['class'])
+ if job_class.respond_to?(:sidekiq_interruptions_exhausted)
+ job_class.interruptions_exhausted_block.call(msg)
+ end
+ rescue => e
+ Sidekiq.logger.error(
+ message: 'Failed to call sidekiq_interruption_exhausted',
+ class: msg['class'],
+ jid: msg['jid'],
+ exception: {
+ class: e.class.name,
+ message: e.message
+ }
+ )
+ end
+
job = Sidekiq.dump_json(msg)
@interrupted_set.put(job, connection: multi_connection)
end
diff --git a/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/interruptions_exhausted.rb b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/interruptions_exhausted.rb
new file mode 100644
index 00000000000..2bc0f7d5c23
--- /dev/null
+++ b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/interruptions_exhausted.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Sidekiq
+ module InterruptionsExhausted
+ def self.included(base)
+ base.extend(ClassMethods)
+ end
+
+ module ClassMethods
+ def sidekiq_interruptions_exhausted(&block)
+ @interruptions_exhausted_block = block
+ end
+
+ def interruptions_exhausted_block
+ @interruptions_exhausted_block
+ end
+ end
+ end
+end
diff --git a/vendor/gems/sidekiq-reliable-fetch/spec/base_reliable_fetch_spec.rb b/vendor/gems/sidekiq-reliable-fetch/spec/base_reliable_fetch_spec.rb
index 3671a8da39c..11890a6442c 100644
--- a/vendor/gems/sidekiq-reliable-fetch/spec/base_reliable_fetch_spec.rb
+++ b/vendor/gems/sidekiq-reliable-fetch/spec/base_reliable_fetch_spec.rb
@@ -3,6 +3,7 @@ require 'fetch_shared_examples'
require 'sidekiq/base_reliable_fetch'
require 'sidekiq/reliable_fetch'
require 'sidekiq/semi_reliable_fetch'
+require 'sidekiq/interruptions_exhausted'
require 'sidekiq/capsule'
describe Sidekiq::BaseReliableFetch do
@@ -72,6 +73,8 @@ describe Sidekiq::BaseReliableFetch do
end
it 'puts jobs into interrupted queue' do
+ stub_const 'Bob', Class.new
+
uow = described_class::UnitOfWork
interrupted_job = Sidekiq.dump_json(class: 'Bob', args: [1, 2, 'foo'], interrupted_count: 3)
jobs = [ uow.new('queue:foo', interrupted_job), uow.new('queue:foo', job), uow.new('queue:bar', job) ]
@@ -82,6 +85,77 @@ describe Sidekiq::BaseReliableFetch do
expect(Sidekiq::InterruptedSet.new.size).to eq 1
end
+ context 'when sidekiq_interruption_exhausted callback is defined' do
+ before do
+ stub_const 'Bob', Class.new
+
+ Bob.class_eval do
+ include Sidekiq::InterruptionsExhausted
+
+ sidekiq_interruptions_exhausted do |msg|
+ self.handle_interruptions_exhausted(msg)
+ end
+
+ # mock method to test interruptions exhausted behavior
+ def self.handle_interruptions_exhausted(msg); end
+ end
+ end
+
+ it 'calls sidekiq_interruption_exhausted callback and sends job to the InterruptedSet' do
+ expect(Bob).to receive(:handle_interruptions_exhausted).with(
+ { 'args' => [1, 2, 'foo'], 'class' => 'Bob', 'interrupted_count' => 4 }
+ )
+
+ uow = described_class::UnitOfWork
+ interrupted_job = Sidekiq.dump_json(class: 'Bob', args: [1, 2, 'foo'], interrupted_count: 3)
+ jobs = [uow.new('queue:foo', interrupted_job), uow.new('queue:foo', job), uow.new('queue:bar', job)]
+ described_class.new(capsule).bulk_requeue(jobs)
+
+ expect(queue1.size).to eq 1
+ expect(queue2.size).to eq 1
+ expect(Sidekiq::InterruptedSet.new.size).to eq 1
+ end
+
+ context 'when an exception is raised during the excecution of the sidekiq_interruptions_exhausted callback' do
+ it 'logs the error and still sends job to the InterruptedSet' do
+ expect(Bob).to receive(:handle_interruptions_exhausted).and_raise(StandardError, 'Error!')
+ expect(Sidekiq.logger).to receive(:error).with(
+ message: 'Failed to call sidekiq_interruption_exhausted',
+ class: 'Bob',
+ jid: nil,
+ exception: {
+ class: 'StandardError',
+ message: 'Error!'
+ }
+ )
+
+ uow = described_class::UnitOfWork
+ interrupted_job = Sidekiq.dump_json(class: 'Bob', args: [1, 2, 'foo'], interrupted_count: 3)
+ jobs = [ uow.new('queue:foo', interrupted_job), uow.new('queue:foo', job), uow.new('queue:bar', job)]
+ described_class.new(capsule).bulk_requeue(jobs)
+
+ expect(queue1.size).to eq 1
+ expect(queue2.size).to eq 1
+ expect(Sidekiq::InterruptedSet.new.size).to eq 1
+ end
+ end
+
+ context 'when interrupted_count is less than max_retries_after_interruption' do
+ it 'does not call sidekiq_interruption_exhausted callback' do
+ expect(Bob).not_to receive(:handle_interruptions_exhausted)
+
+ uow = described_class::UnitOfWork
+ interrupted_job = Sidekiq.dump_json(class: 'Bob', args: [1, 2, 'foo'], interrupted_count: 1)
+ jobs = [ uow.new('queue:foo', interrupted_job), uow.new('queue:foo', job), uow.new('queue:bar', job)]
+ described_class.new(capsule).bulk_requeue(jobs)
+
+ expect(queue1.size).to eq 2
+ expect(queue2.size).to eq 1
+ expect(Sidekiq::InterruptedSet.new.size).to eq 0
+ end
+ end
+ end
+
context 'when max_retries_after_interruption is disabled' do
let(:options) { { queues: queues, max_retries_after_interruption: -1 } }
diff --git a/yarn.lock b/yarn.lock
index f3d56586b36..4e3925cc4da 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5556,10 +5556,10 @@ core-js-pure@^3.30.2:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.35.0.tgz#4660033304a050215ae82e476bd2513a419fbb34"
integrity sha512-f+eRYmkou59uh7BPcyJ8MC76DiGhspj1KMxVIcF24tzP8NA9HVa1uC7BTW2tgx7E1QVCzDzsgp7kArrzhlz8Ew==
-core-js@^3.29.1, core-js@^3.38.1, core-js@^3.6.5:
- version "3.38.1"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.38.1.tgz#aa375b79a286a670388a1a363363d53677c0383e"
- integrity sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==
+core-js@^3.29.1, core-js@^3.39.0, core-js@^3.6.5:
+ version "3.39.0"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.39.0.tgz#57f7647f4d2d030c32a72ea23a0555b2eaa30f83"
+ integrity sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==
core-util-is@~1.0.0:
version "1.0.3"
|