Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-06-13 09:12:50 +00:00
parent 487dd921ae
commit 4badf32da7
33 changed files with 513 additions and 197 deletions

View File

@ -64,7 +64,6 @@ Database/JsonbSizeLimit:
- 'ee/app/models/package_metadata/affected_package.rb'
- 'ee/app/models/package_metadata/package.rb'
- 'ee/app/models/projects/xray_report.rb'
- 'ee/app/models/remote_development/workspaces_agent_config.rb'
- 'ee/app/models/sbom/occurrence.rb'
- 'ee/app/models/sbom/source.rb'
- 'ee/app/models/search/elastic/reindexing_task.rb'
@ -82,7 +81,6 @@ Database/JsonbSizeLimit:
- 'ee/app/models/security/vulnerability_management_policy_rule.rb'
- 'ee/app/models/vulnerabilities/archived_record.rb'
- 'ee/app/models/vulnerabilities/finding.rb'
- 'ee/lib/remote_development/workspace_operations/desired_config.rb'
- 'lib/gitlab/background_migration/backfill_draft_status_on_merge_requests_with_corrected_regex.rb'
- 'lib/gitlab/background_migration/migrate_links_for_vulnerability_findings.rb'
- 'lib/gitlab/background_migration/purge_stale_security_scans.rb'

View File

@ -1 +1 @@
7722e3619144b4e551afe08b52782278efd18920
8ce0f90e5793c02397a896740d2317efbfd9cde3

View File

@ -31,6 +31,11 @@ export default {
error: i18n.errors.playJob,
mutation: playJobMutation,
},
stop: {
dataName: 'jobPlay',
error: i18n.errors.playJob,
mutation: playJobMutation,
},
retry: {
dataName: 'jobRetry',
error: i18n.errors.retryJob,

View File

@ -12,6 +12,7 @@ import { __, s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { localeDateFormat } from '~/lib/utils/datetime/locale_dateformat';
import { isFinished } from '~/deployments/utils';
import DeploymentStatusLink from './deployment_status_link.vue';
import Commit from './commit.vue';
@ -99,7 +100,8 @@ export default {
return this.ref?.refPath;
},
needsApproval() {
return this.deployment.pendingApprovalCount > 0;
const deploymentStatus = this.status ? this.status.toUpperCase() : '';
return !isFinished({ status: deploymentStatus }) && this.deployment.pendingApprovalCount > 0;
},
},
i18n: {

View File

@ -228,7 +228,7 @@ export default {
},
i18n: {
selectReviewer: __('Select reviewer'),
unassign: __('Unassign'),
unassign: __('Unassign all'),
},
};
</script>

View File

@ -21,10 +21,14 @@ module Groups
respond_to do |format|
format.json do
serializer = GroupChildSerializer
.new(current_user: current_user)
.with_pagination(request, response)
serializer.expand_hierarchy(parent) if params[:filter].present?
render json: serializer.represent(children)
.new(current_user: current_user)
.with_pagination(request, response)
serializer.expand_hierarchy(parent) if expand_hierarchy?
render json: serializer.represent(children, {
upto_preloaded_ancestors_only: expand_inactive_hierarchy?
})
end
end
end
@ -58,9 +62,18 @@ module Groups
archived: Gitlab::Utils.to_boolean(safe_params[:archived], default: safe_params[:archived]),
not_aimed_for_deletion: Gitlab::Utils.to_boolean(safe_params[:not_aimed_for_deletion])
)
params_copy.delete(:active) unless filter_active?
params_copy.delete(:active) unless Feature.enabled?(:group_descendants_active_filter, current_user)
params_copy.compact
end
strong_memoize_attr :descendants_params
def expand_inactive_hierarchy?
descendants_params[:active] == false
end
def expand_hierarchy?
descendants_params[:filter] || expand_inactive_hierarchy?
end
def validate_per_page
return unless params.key?(:per_page)
@ -77,11 +90,5 @@ module Groups
end
end
end
def filter_active?
return false unless Feature.enabled?(:group_descendants_active_filter, current_user)
parent.active?
end
end
end

View File

@ -5,9 +5,6 @@
# Used to find and filter all subgroups and projects of a passed parent group
# visible to a specified user.
#
# When passing a `filter` param, the search is performed over all nested levels
# of the `parent_group`. All ancestors for a search result are loaded
#
# Arguments:
# current_user: The user for which the children should be visible
# parent_group: The group to find children of
@ -15,9 +12,14 @@
# Supports all params that the `ProjectsFinder` and `GroupProjectsFinder`
# support.
#
# active: boolean - filters for active descendants.
# filter: string - is aliased to `search` for consistency with the frontend.
# active: boolean - filters for active descendants. When `false`, the search is performed over
# all nested levels of the `parent group` and all inactive ancestors are loaded.
# filter: string - aliased to `search` for consistency with the frontend. When a filter is
# passed, the search is performed over all nested levels of the `parent_group`.
# All ancestors for a search result are loaded
class GroupDescendantsFinder
include Gitlab::Utils::StrongMemoize
attr_reader :current_user, :parent_group, :params
def initialize(parent_group:, current_user: nil, params: {})
@ -34,7 +36,7 @@ class GroupDescendantsFinder
.page(page)
preloaded_ancestors = []
if params[:filter]
if search_descendants?
preloaded_ancestors |= ancestors_of_filtered_subgroups
preloaded_ancestors |= ancestors_of_filtered_projects
end
@ -62,28 +64,11 @@ class GroupDescendantsFinder
.execute
end
# rubocop: disable CodeReuse/ActiveRecord
def all_visible_descendant_groups
groups_table = Group.arel_table
visible_to_user = groups_table[:visibility_level]
.in(Gitlab::VisibilityLevel.levels_for_user(current_user))
if current_user
authorized_groups = GroupsFinder.new(current_user, all_available: false) # rubocop: disable CodeReuse/Finder
.execute.arel.as('authorized')
authorized_to_user = groups_table.project(1).from(authorized_groups)
.where(authorized_groups[:id].eq(groups_table[:id]))
.exists
visible_to_user = visible_to_user.or(authorized_to_user)
end
parent_group.descendants.where(visible_to_user)
end
# rubocop: enable CodeReuse/ActiveRecord
def subgroups_matching_filter
all_visible_descendant_groups
.search(params[:filter])
def descendant_groups
descendants = parent_group.descendants
descendants = by_visible_to_users(descendants)
descendants = by_active(descendants)
by_search(descendants)
end
# When filtering we want all to preload all the ancestors upto the specified
@ -97,7 +82,9 @@ class GroupDescendantsFinder
# So when searching 'project', on the 'subgroup' page we want to preload
# 'nested-group' but not 'subgroup' or 'root'
def ancestors_of_groups(base_for_ancestors)
Group.id_in(base_for_ancestors).self_and_ancestors(upto: parent_group.id)
ancestors = Group.id_in(base_for_ancestors).self_and_ancestors(upto: parent_group.id)
ancestors = ancestors.self_or_ancestors_inactive if inactive?
ancestors
end
def ancestors_of_filtered_projects
@ -116,8 +103,8 @@ class GroupDescendantsFinder
def subgroups
# When filtering subgroups, we want to find all matches within the tree of
# descendants to show to the user
groups = if params[:filter]
subgroups_matching_filter
groups = if search_descendants?
descendant_groups
else
direct_child_groups
end
@ -133,20 +120,22 @@ class GroupDescendantsFinder
# Finds all projects nested under `parent_group` or any of its descendant
# groups
def projects_matching_filter
def descendant_projects
projects_nested_in_group = Project.in_namespace(parent_group.self_and_descendants.as_ids)
params_with_search = params.merge(search: params[:filter])
finder_params = params.dup
finder_params[:search] = params[:filter] if params[:filter]
ProjectsFinder.new( # rubocop:disable CodeReuse/Finder
params: params_with_search,
params: finder_params,
current_user: current_user,
project_ids_relation: projects_nested_in_group
).execute
end
def projects
projects = if params[:filter]
projects_matching_filter
projects = if search_descendants?
descendant_projects
else
direct_child_projects
end
@ -177,4 +166,51 @@ class GroupDescendantsFinder
def page
params[:page].to_i
end
# Filters group descendants to only include those visible to the current user.
#
# This method applies visibility filtering based on two criteria:
# 1. Groups with visibility level accessible to the current user
# 2. Groups where the user has explicit authorization (if authenticated)
#
# @param descendants [ActiveRecord::Relation<Group>] The collection of group descendants to filter
# @return [ActiveRecord::Relation<Group>] Filtered descendants visible to the current user
# rubocop: disable CodeReuse/ActiveRecord -- Needs specialized queries for optimization
def by_visible_to_users(descendants)
groups_table = Group.arel_table
visible_to_user = groups_table[:visibility_level]
.in(Gitlab::VisibilityLevel.levels_for_user(current_user))
if current_user
authorized_groups = GroupsFinder.new(current_user, all_available: false) # rubocop: disable CodeReuse/Finder
.execute.arel.as('authorized')
authorized_to_user = groups_table.project(1).from(authorized_groups)
.where(authorized_groups[:id].eq(groups_table[:id]))
.exists
visible_to_user = visible_to_user.or(authorized_to_user)
end
descendants.where(visible_to_user)
end
# rubocop: enable CodeReuse/ActiveRecord
def by_active(descendants)
return descendants if params[:active].nil?
params[:active] ? descendants.self_and_ancestors_active : descendants.self_or_ancestors_inactive
end
def by_search(descendants)
return descendants unless params[:filter]
descendants.search(params[:filter])
end
def inactive?
params[:active] == false
end
def search_descendants?
params[:filter].present? || inactive?
end
end

View File

@ -136,6 +136,12 @@ module Types
null: true,
description: 'Indicates the archived status of the project.'
field :marked_for_deletion, GraphQL::Types::Boolean,
null: true,
description: 'Indicates if the project or any ancestor is scheduled for deletion.',
method: :scheduled_for_deletion_in_hierarchy_chain?,
experiment: { milestone: '18.1' }
field :marked_for_deletion_on, ::Types::TimeType,
null: true,
description: 'Date when project was scheduled to be deleted.',

View File

@ -4,20 +4,30 @@ module GroupDescendant
# Returns the hierarchy of a project or group in the from of a hash upto a
# given top.
#
# Options:
# upto_preloaded_ancestors_only: boolean - When `true`, the hierarchy expansions stops at the
# highest level preloaded ancestor. The hierarchy isn't
# guaranteed to reach the `hierarchy_top`.
#
# > project.hierarchy
# => { parent_group => { child_group => project } }
def hierarchy(hierarchy_top = nil, preloaded = nil)
def hierarchy(hierarchy_top = nil, preloaded = nil, opts = {})
preloaded ||= ancestors_upto(hierarchy_top)
expand_hierarchy_for_child(self, self, hierarchy_top, preloaded)
expand_hierarchy_for_child(self, self, hierarchy_top, preloaded, opts)
end
# Merges all hierarchies of the given groups or projects into an array of
# hashes. All ancestors need to be loaded into the given `descendants` to avoid
# queries down the line.
#
# Options:
# upto_preloaded_ancestors_only: boolean - When `true`, the hierarchy expansions stops at the
# highest level preloaded ancestor. The hierarchy isn't
# guaranteed to reach the `hierarchy_top`.
#
# > GroupDescendant.merge_hierarchy([project, child_group, child_group2, parent])
# => { parent => [{ child_group => project}, child_group2] }
def self.build_hierarchy(descendants, hierarchy_top = nil)
def self.build_hierarchy(descendants, hierarchy_top = nil, opts = {})
descendants = Array.wrap(descendants).uniq
return [] if descendants.empty?
@ -26,7 +36,7 @@ module GroupDescendant
end
all_hierarchies = descendants.map do |descendant|
descendant.hierarchy(hierarchy_top, descendants)
descendant.hierarchy(hierarchy_top, descendants, opts)
end
Gitlab::Utils::MergeHash.merge(all_hierarchies)
@ -34,39 +44,50 @@ module GroupDescendant
private
def expand_hierarchy_for_child(child, hierarchy, hierarchy_top, preloaded)
def expand_hierarchy_for_child(child, hierarchy, hierarchy_top, preloaded, opts = {})
parent = hierarchy_top if hierarchy_top && child.parent_id == hierarchy_top.id
parent ||= preloaded.detect do |possible_parent|
possible_parent.is_a?(Group) && possible_parent.id == child.parent_id
end
if parent.nil? && !child.parent_id.nil?
parent = child.parent
exception = ArgumentError.new <<~MSG
Parent was not preloaded for child when rendering group hierarchy.
This error is not user facing, but causes a +1 query.
MSG
exception.set_backtrace(caller)
extras = {
parent: parent.inspect,
child: child.inspect,
preloaded: preloaded.map(&:full_path),
issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/49404'
}
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception, extras)
end
if parent.nil? && hierarchy_top.present?
raise ArgumentError, _('specified top is not part of the tree')
unless opts[:upto_preloaded_ancestors_only]
parent ||= load_parent!(child, preloaded)
validate_hierarchy_top_in_tree!(parent, hierarchy_top)
end
if parent && parent != hierarchy_top
expand_hierarchy_for_child(parent, { parent => hierarchy }, hierarchy_top, preloaded)
expand_hierarchy_for_child(parent, { parent => hierarchy }, hierarchy_top, preloaded, opts)
else
hierarchy
end
end
def load_parent!(child, preloaded)
return if child.parent_id.nil?
parent = child.parent
exception = ArgumentError.new <<~MSG
Parent was not preloaded for child when rendering group hierarchy.
This error is not user facing, but causes a +1 query.
MSG
exception.set_backtrace(caller)
extras = {
parent: parent.inspect,
child: child.inspect,
preloaded: preloaded.map(&:full_path),
issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/49404'
}
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(exception, extras)
parent
end
def validate_hierarchy_top_in_tree!(parent, hierarchy_top)
return if parent.present? || hierarchy_top.nil?
raise ArgumentError, _('specified top is not part of the tree')
end
end

View File

@ -29,7 +29,7 @@ class GroupChildSerializer < BaseSerializer
if children.is_a?(GroupDescendant)
represent_hierarchy(children.hierarchy(hierarchy_root), opts).first
else
hierarchies = GroupDescendant.build_hierarchy(children, hierarchy_root)
hierarchies = GroupDescendant.build_hierarchy(children, hierarchy_root, opts)
# When an array was passed, we always want to represent an array.
# Even if the hierarchy only contains one element
represent_hierarchy(Array.wrap(hierarchies), opts)

View File

@ -9,24 +9,7 @@ description: Used to store information of the exported files containing the data
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59976
milestone: '13.12'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: export_id
table: bulk_import_exports
sharding_key: project_id
belongs_to: export
group_id:
references: namespaces
backfill_via:
parent:
foreign_key: export_id
table: bulk_import_exports
sharding_key: group_id
belongs_to: export
sharding_key:
project_id: projects
group_id: namespaces
table_size: small
desired_sharding_key_migration_job_name:
- BackfillBulkImportExportUploadsProjectId
- BackfillBulkImportExportUploadsGroupId

View File

@ -9,14 +9,6 @@ description: Persists information about on-call rotation participants
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/49058
milestone: '13.7'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: oncall_rotation_id
table: incident_management_oncall_rotations
sharding_key: project_id
belongs_to: rotation
sharding_key:
project_id: projects
table_size: small
desired_sharding_key_migration_job_name: BackfillIncidentManagementOncallParticipantsProjectId

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddIncidentManagementOncallParticipantsProjectIdNotNull < Gitlab::Database::Migration[2.3]
milestone '18.1'
disable_ddl_transaction!
def up
add_not_null_constraint :incident_management_oncall_participants, :project_id
end
def down
remove_not_null_constraint :incident_management_oncall_participants, :project_id
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddMultiColumnNotNullConstraintToBulkImportExportUploads < Gitlab::Database::Migration[2.3]
milestone '18.1'
disable_ddl_transaction!
def up
add_multi_column_not_null_constraint(:bulk_import_export_uploads, :project_id, :group_id)
end
def down
remove_multi_column_not_null_constraint(:bulk_import_export_uploads, :project_id, :group_id)
end
end

View File

@ -0,0 +1 @@
406d7ba5413e155fcaa64ac630472b88ba75d6d9413a161f1cd4fb59521880af

View File

@ -0,0 +1 @@
4c8cdcd364c8eb3fc83b0fde13039de700f0e58003c508ddbe662a0a76d9977d

View File

@ -10745,7 +10745,8 @@ CREATE TABLE bulk_import_export_uploads (
batch_id bigint,
project_id bigint,
group_id bigint,
CONSTRAINT check_5add76239d CHECK ((char_length(export_file) <= 255))
CONSTRAINT check_5add76239d CHECK ((char_length(export_file) <= 255)),
CONSTRAINT check_e1d215df28 CHECK ((num_nonnulls(group_id, project_id) = 1))
);
CREATE SEQUENCE bulk_import_export_uploads_id_seq
@ -15637,7 +15638,8 @@ CREATE TABLE incident_management_oncall_participants (
color_palette smallint NOT NULL,
color_weight smallint NOT NULL,
is_removed boolean DEFAULT false NOT NULL,
project_id bigint
project_id bigint,
CONSTRAINT check_d53b689825 CHECK ((project_id IS NOT NULL))
);
CREATE SEQUENCE incident_management_oncall_participants_id_seq

View File

@ -15140,6 +15140,29 @@ The connection type for [`ComplianceFramework`](#complianceframework).
| <a id="complianceframeworkconnectionnodes"></a>`nodes` | [`[ComplianceFramework]`](#complianceframework) | A list of nodes. |
| <a id="complianceframeworkconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `ComplianceFrameworkCoverageDetailConnection`
The connection type for [`ComplianceFrameworkCoverageDetail`](#complianceframeworkcoveragedetail).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="complianceframeworkcoveragedetailconnectionedges"></a>`edges` | [`[ComplianceFrameworkCoverageDetailEdge]`](#complianceframeworkcoveragedetailedge) | A list of edges. |
| <a id="complianceframeworkcoveragedetailconnectionnodes"></a>`nodes` | [`[ComplianceFrameworkCoverageDetail]`](#complianceframeworkcoveragedetail) | A list of nodes. |
| <a id="complianceframeworkcoveragedetailconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `ComplianceFrameworkCoverageDetailEdge`
The edge type for [`ComplianceFrameworkCoverageDetail`](#complianceframeworkcoveragedetail).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="complianceframeworkcoveragedetailedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="complianceframeworkcoveragedetailedgenode"></a>`node` | [`ComplianceFrameworkCoverageDetail`](#complianceframeworkcoveragedetail) | The item at the end of the edge. |
#### `ComplianceFrameworkEdge`
The edge type for [`ComplianceFramework`](#complianceframework).
@ -24478,6 +24501,18 @@ four standard [pagination arguments](#pagination-arguments):
| <a id="complianceframeworkpipelineexecutionschedulepoliciesincludeunscoped"></a>`includeUnscoped` | [`Boolean`](#boolean) | Filter policies that are scoped to the project. |
| <a id="complianceframeworkpipelineexecutionschedulepoliciesrelationship"></a>`relationship` | [`SecurityPolicyRelationType`](#securitypolicyrelationtype) | Filter policies by the given policy relationship. Default is DIRECT. |
### `ComplianceFrameworkCoverageDetail`
Framework coverage details for a specific compliance framework.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="complianceframeworkcoveragedetailcoveredcount"></a>`coveredCount` | [`Int!`](#int) | Number of projects covered by the framework. |
| <a id="complianceframeworkcoveragedetailid"></a>`id` | [`ID!`](#id) | ID of the framework. |
| <a id="complianceframeworkcoveragedetailname"></a>`name` | [`String!`](#string) | Name of the framework. |
### `ComplianceFrameworkCoverageSummary`
Compliance framework Coverage summary for a group.
@ -28198,6 +28233,7 @@ GPG signature for a signed commit.
| <a id="groupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
| <a id="groupcicdsettings"></a>`ciCdSettings` {{< icon name="warning-solid" >}} | [`CiCdSettings`](#cicdsettings) | **Introduced** in GitLab 17.9. **Status**: Experiment. Namespace CI/CD settings for the namespace. |
| <a id="groupcomplianceframeworkcoveragesummary"></a>`complianceFrameworkCoverageSummary` {{< icon name="warning-solid" >}} | [`ComplianceFrameworkCoverageSummary`](#complianceframeworkcoveragesummary) | **Introduced** in GitLab 18.1. **Status**: Experiment. Summary of compliance framework coverage in a group and its subgroups. |
| <a id="groupcomplianceframeworkscoveragedetails"></a>`complianceFrameworksCoverageDetails` {{< icon name="warning-solid" >}} | [`ComplianceFrameworkCoverageDetailConnection`](#complianceframeworkcoveragedetailconnection) | **Introduced** in GitLab 18.1. **Status**: Experiment. Detailed compliance framework coverage for each framework in the group. |
| <a id="groupcompliancerequirementcontrolcoverage"></a>`complianceRequirementControlCoverage` {{< icon name="warning-solid" >}} | [`RequirementControlCoverage`](#requirementcontrolcoverage) | **Introduced** in GitLab 18.1. **Status**: Experiment. Compliance control status summary showing count of passed, failed, and pending controls. |
| <a id="groupcontainerrepositoriescount"></a>`containerRepositoriesCount` | [`Int!`](#int) | Number of container repositories in the group. |
| <a id="groupcontainslockedprojects"></a>`containsLockedProjects` | [`Boolean`](#boolean) | Includes at least one project where the repository size exceeds the limit. This only applies to namespaces under Project limit enforcement. |
@ -35983,6 +36019,7 @@ Project-level settings for product analytics provider.
| <a id="projectlanguages"></a>`languages` | [`[RepositoryLanguage!]`](#repositorylanguage) | Programming languages used in the project. |
| <a id="projectlastactivityat"></a>`lastActivityAt` | [`Time`](#time) | Timestamp of the project last activity. |
| <a id="projectlfsenabled"></a>`lfsEnabled` | [`Boolean`](#boolean) | Indicates if the project has Large File Storage (LFS) enabled. |
| <a id="projectmarkedfordeletion"></a>`markedForDeletion` {{< icon name="warning-solid" >}} | [`Boolean`](#boolean) | **Introduced** in GitLab 18.1. **Status**: Experiment. Indicates if the project or any ancestor is scheduled for deletion. |
| <a id="projectmarkedfordeletionon"></a>`markedForDeletionOn` {{< icon name="warning-solid" >}} | [`Time`](#time) | **Introduced** in GitLab 16.10. **Status**: Experiment. Date when project was scheduled to be deleted. |
| <a id="projectmaxaccesslevel"></a>`maxAccessLevel` | [`AccessLevel!`](#accesslevel) | Maximum access level of the current user in the project. |
| <a id="projectmergecommittemplate"></a>`mergeCommitTemplate` | [`String`](#string) | Template used to create merge commit message in merge requests. |
@ -43645,6 +43682,7 @@ AI features that can be configured through the Model Selection feature settings.
| <a id="aimodelselectionfeaturesduo_chat_write_tests"></a>`DUO_CHAT_WRITE_TESTS` | Duo chat write test feature setting. |
| <a id="aimodelselectionfeaturesgenerate_commit_message"></a>`GENERATE_COMMIT_MESSAGE` | Generate commit message feature setting. |
| <a id="aimodelselectionfeaturesresolve_vulnerability"></a>`RESOLVE_VULNERABILITY` | Resolve vulnerability feature setting. |
| <a id="aimodelselectionfeaturesreview_merge_request"></a>`REVIEW_MERGE_REQUEST` | Review merge request feature setting. |
| <a id="aimodelselectionfeaturessummarize_new_merge_request"></a>`SUMMARIZE_NEW_MERGE_REQUEST` | Summarize new merge request feature setting. |
| <a id="aimodelselectionfeaturessummarize_review"></a>`SUMMARIZE_REVIEW` | Summarize review feature setting. |

View File

@ -12,9 +12,8 @@ the most out of GitLab.
| Topic | Description | Good for beginners |
|-------|-------------|--------------------|
| [Make your first Git commit](make_first_git_commit/_index.md) | Create a project, edit a file, and commit changes to a Git repository from the command line. | {{< icon name="star" >}} |
| [Start using Git on the command line](../topics/git/commands.md) | Learn how to set up Git, clone repositories, and work with branches. | {{< icon name="star" >}} |
| [Introduction to Git](https://university.gitlab.com/courses/introduction-to-git) | Learn the basics of Git in this self-paced course. | {{< icon name="star" >}} |
| [Take advantage of Git rebase](https://about.gitlab.com/blog/2022/10/06/take-advantage-of-git-rebase/) | Learn how to use the `rebase` command in your workflow. | |
| [Update Git commit messages](update_commit_messages/_index.md) | Learn how to update commit messages and push the changes to GitLab. | |
| [Update Git remote URLs](update_git_remote_url/_index.md) | Learn how to update the Git remote URLs in your local project when they have changed. | |
| [Git cheat sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf) | Download a PDF of common Git commands. | |
| [Git-ing started with Git](https://www.youtube.com/watch?v=Ce5nz5n41z4) | Git basics video tutorial. | |
| [GitLab source code management walkthrough](https://www.youtube.com/watch?v=wTQ3aXJswtM) | GitLab workflow video tutorial. | |

View File

@ -95,3 +95,6 @@ Your code goes through a pre-scan security workflow when using GitLab Duo:
When you are using [GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/_index.md)
and the self-hosted AI gateway, you do not share any data with GitLab.
GitLab Self-Managed administrators can use [Service Ping](../../administration/settings/usage_statistics.md#service-ping)
to send usage statistics to GitLab. This is separate to the [telemetry data](#telemetry).

View File

@ -34,7 +34,7 @@ If you do not select a specific LLM, the AI-native features use the GitLab-selec
{{< alert type="note" >}}
To maintain optimal performance and reliability, GitLab might change the default LLM without notifying the user.
To maintain optimal performance and reliability, GitLab might change the default LLM without notifying the user. GitLab does not change non-default LLMs that have been explicitly selected.
{{< /alert >}}

View File

@ -304,6 +304,14 @@ To install the Helm chart for the GitLab workspaces proxy:
1. Install the chart:
{{< alert type="note" >}}
Before chart version 0.1.16, the Helm chart installation created secrets automatically.
If you're upgrading from a version earlier than 0.1.16,
[create the required Kubernetes secrets](#create-kubernetes-secrets) before running the upgrade command.
{{< /alert >}}
```shell
helm repo update

View File

@ -65501,7 +65501,7 @@ msgstr ""
msgid "Unarchiving the project restores its members' ability to make commits, and create issues, comments, and other entities. %{strong_start}After you unarchive the project, it displays in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}"
msgstr ""
msgid "Unassign"
msgid "Unassign all"
msgstr ""
msgid "Unassign from commenting user"

View File

@ -253,86 +253,103 @@ RSpec.describe Groups::ChildrenController, feature_category: :groups_and_project
context 'with active parameter' do
let_it_be(:group) { create(:group) }
let_it_be(:active_child) { create(:group, parent: group) }
let_it_be(:active_subgroup) { create(:group, parent: group) }
let_it_be(:active_project) { create(:project, :public, group: group) }
let_it_be(:marked_for_deletion_child) { create(:group_with_deletion_schedule, parent: group) }
let_it_be(:marked_for_deletion_project) do
create(:project, :public, group: group, marked_for_deletion_at: Date.current)
end
let_it_be(:inactive_subgroup) { create(:group, :archived, parent: group) }
let_it_be(:inactive_project) { create(:project, :archived, :public, group: group) }
let_it_be(:archived_project) { create(:project, :archived, :public, group: group) }
let_it_be(:archived_child) do
create(:group, parent: group, namespace_settings: create(:namespace_settings, archived: true))
end
subject(:make_request) { get :index, params: { group_id: group.to_param, active: active_param }, format: :json }
shared_examples 'endpoint that returns all child' do
it 'returns all child', :aggregate_failures do
make_request
expect(response).to have_gitlab_http_status(:ok)
expect(descendant_ids(json_response))
.to contain_exactly(
active_child.id,
marked_for_deletion_child.id,
marked_for_deletion_project.id,
archived_child.id,
archived_project.id
)
end
end
context 'when true' do
subject(:make_request) { get :index, params: { group_id: group.to_param, active: true }, format: :json }
it 'returns active child', :aggregate_failures do
make_request
expect(response).to have_gitlab_http_status(:ok)
expect(descendant_ids(json_response)).to contain_exactly(active_child.id)
end
context 'when group_descendants_active_filter flag is disabled' do
before do
stub_feature_flags(group_descendants_active_filter: false)
end
it_behaves_like 'endpoint that returns all child'
end
context 'when group is inactive' do
let_it_be(:deletion_schedule) { create(:group_deletion_schedule, group: group) }
it_behaves_like 'endpoint that returns all child'
end
end
context 'when false' do
subject(:make_request) { get :index, params: { group_id: group.to_param, active: false }, format: :json }
it 'returns inactive child', :aggregate_failures do
shared_examples 'request with no parameter' do
it 'returns direct child', :aggregate_failures do
make_request
expect(response).to have_gitlab_http_status(:ok)
expect(descendant_ids(json_response)).to contain_exactly(
marked_for_deletion_child.id,
marked_for_deletion_project.id,
archived_child.id,
archived_project.id
active_subgroup.id,
active_project.id,
inactive_subgroup.id,
inactive_project.id
)
end
end
context 'when group_descendants_active_filter flag is disabled' do
context 'when true' do
let_it_be(:active_param) { true }
it 'returns active direct children', :aggregate_failures do
make_request
expect(response).to have_gitlab_http_status(:ok)
expect(descendant_ids(json_response)).to include(active_subgroup.id, active_project.id)
expect(descendant_ids(json_response)).not_to include(inactive_subgroup.id, inactive_project.id)
end
context 'when `group_descendants_active_filter` flag is disabled' do
before do
stub_feature_flags(group_descendants_active_filter: false)
end
it_behaves_like 'endpoint that returns all child'
it_behaves_like 'request with no parameter'
end
end
context 'when false' do
let_it_be(:active_param) { false }
it 'returns inactive direct children', :aggregate_failures do
make_request
expect(response).to have_gitlab_http_status(:ok)
expect(descendant_ids(json_response)).to include(inactive_subgroup.id, inactive_project.id)
expect(descendant_ids(json_response)).not_to include(active_subgroup.id, active_project.id)
end
context 'when group is inactive' do
let_it_be(:deletion_schedule) { create(:group_deletion_schedule, group: group) }
context 'when active subgroup has children' do
let_it_be(:active_descendant_group) { create(:group, parent: active_subgroup) }
let_it_be(:active_descendant_project) { create(:project, :public, group: active_subgroup) }
it_behaves_like 'endpoint that returns all child'
let_it_be(:inactive_descendant_group) { create(:group, :archived, parent: active_subgroup) }
let_it_be(:inactive_descendant_project) { create(:project, :public, :archived, group: active_subgroup) }
it 'returns inactive descendants', :aggregate_failures do
make_request
expect(response).to have_gitlab_http_status(:ok)
expect(descendant_ids(json_response))
.to include(inactive_descendant_group.id, inactive_descendant_project.id)
expect(descendant_ids(json_response))
.not_to include(active_descendant_group.id, active_descendant_project.id)
end
end
context 'when inactive subgroup has children' do
let_it_be(:active_descendant_group) { create(:group, parent: inactive_subgroup) }
let_it_be(:active_descendant_project) { create(:project, :public, group: inactive_subgroup) }
let_it_be(:inactive_descendant_group) { create(:group, :archived, parent: inactive_subgroup) }
let_it_be(:inactive_descendant_project) { create(:project, :public, :archived, group: inactive_subgroup) }
it 'returns all descendants' do
make_request
expect(response).to have_gitlab_http_status(:ok)
expect(descendant_ids(json_response)).to include(
active_descendant_group.id,
active_descendant_project.id,
inactive_descendant_group.id,
inactive_descendant_project.id
)
end
end
context 'when `group_descendants_active_filter` flag is disabled' do
before do
stub_feature_flags(group_descendants_active_filter: false)
end
it_behaves_like 'request with no parameter'
end
end
end

View File

@ -75,25 +75,76 @@ RSpec.describe GroupDescendantsFinder, feature_category: :groups_and_projects do
end
context 'with active parameter' do
let_it_be(:active_child) { create(:group, parent: group) }
let_it_be(:active_subgroup) { create(:group, parent: group) }
let_it_be(:active_project) { create(:project, group: group) }
let_it_be(:inactive_child) { create(:group_with_deletion_schedule, parent: group) }
let_it_be(:inactive_subgroup) { create(:group, :archived, parent: group) }
let_it_be(:inactive_project) { create(:project, :archived, group: group) }
context 'when parameter is true' do
subject { finder.execute }
context 'when true' do
let(:params) { { active: true } }
it 'returns active children' do
expect(finder.execute).to contain_exactly(active_child, active_project)
is_expected.to contain_exactly(active_subgroup, active_project)
end
end
context 'when parameter is false' do
context 'when false' do
let(:params) { { active: false } }
it 'returns inactive children' do
expect(finder.execute).to contain_exactly(inactive_child, inactive_project)
is_expected.to include(inactive_subgroup, inactive_project)
is_expected.not_to include(active_subgroup, active_project)
end
context 'when subgroup is inactive' do
let_it_be(:inactive_subgroup_subgroup) { create(:group, parent: inactive_subgroup) }
let_it_be(:inactive_subgroup_project) { create(:project, group: inactive_subgroup) }
it 'returns all children' do
is_expected.to include(inactive_subgroup_subgroup, inactive_subgroup_project)
end
end
context 'when subgroup is active' do
let_it_be(:active_subgroup_active_subgroup) { create(:group, parent: active_subgroup) }
let_it_be(:active_subgroup_active_project) { create(:project, group: active_subgroup) }
let_it_be(:active_subgroup_inactive_subgroup) { create(:group, :archived, parent: active_subgroup) }
let_it_be(:active_subgroup_inactive_project) { create(:project, :archived, group: active_subgroup) }
it 'returns inactive descendant', :aggregate_failures do
is_expected.to include(active_subgroup_inactive_subgroup, active_subgroup_inactive_project)
is_expected.not_to include(active_subgroup_active_subgroup, active_subgroup_active_project)
end
end
context 'when matched descendant has inactive ancestor' do
let_it_be(:inactive_descendant) { create(:group, :archived, parent: inactive_subgroup) }
# Filter and page size params ensure only the leaf descendant matches the query,
# so any ancestors must be there due to preloading, not because they happen to be
# a part of the initial results.
let(:params) { { active: false, filter: inactive_descendant.name, per_page: 1 } }
it 'preloads inactive ancestor' do
is_expected.to contain_exactly(inactive_descendant, inactive_subgroup)
end
end
context 'when matches descendant has active ancestor' do
let_it_be(:inactive_descendant) { create(:group, :archived, parent: active_subgroup) }
# Filter and page size params ensure only the leaf descendant matches the query,
# so any ancestors must be there due to preloading, not because they happen to be
# a part of the initial results.
let(:params) { { active: false, filter: inactive_descendant.name, per_page: 1 } }
it 'does not preload active ancestor' do
is_expected.to contain_exactly(inactive_descendant)
end
end
end
end

View File

@ -85,6 +85,7 @@ describe('JobActionButton', () => {
${'run'} | ${'play'} | ${'Run'} | ${1}
${'retry'} | ${'retry'} | ${'Run again'} | ${2}
${'unschedule'} | ${'time-out'} | ${'Unschedule'} | ${3}
${'stop'} | ${'stop'} | ${'Stop'} | ${4}
`('$action action', ({ icon, mockIndex, tooltip }) => {
beforeEach(() => {
createComponent({ props: { jobAction: mockJobActions[mockIndex] } });
@ -109,6 +110,7 @@ describe('JobActionButton', () => {
${'run'} | ${1} | ${playJobMutation} | ${playMutationHandler} | ${i18n.errors.playJob}
${'retry'} | ${2} | ${retryJobMutation} | ${retryMutationHandler} | ${i18n.errors.retryJob}
${'unschedule'} | ${3} | ${unscheduleJobMutation} | ${unscheduleMutationHandler} | ${i18n.errors.unscheduleJob}
${'stop'} | ${4} | ${playJobMutation} | ${playMutationHandler} | ${i18n.errors.playJob}
`('$action action', ({ mockIndex, mutation, handler, errorMessage }) => {
it('calls the correct mutation on button click', async () => {
await createComponent({

View File

@ -119,6 +119,14 @@ export const mockJobActions = [
path: '/flightjs/Flight/-/jobs/1001/unschedule',
title: 'Unschedule',
},
{
__typename: 'StatusAction',
confirmationMessage: null,
id: 'Ci::Build-stop-1001',
icon: 'stop',
path: '/flightjs/Flight/-/jobs/1001/play',
title: 'Stop',
},
];
export const mockJobMutationResponse = (dataName) => ({

View File

@ -67,12 +67,20 @@ describe('~/environments/components/deployment.vue', () => {
describe('approval badge', () => {
it('should show a badge if the deployment needs approval', () => {
wrapper = createWrapper({
propsData: { deployment: { ...deployment, pendingApprovalCount: 5 } },
propsData: { deployment: { ...deployment, pendingApprovalCount: 5, status: 'running' } },
});
expect(findApprovalBadge().exists()).toBe(true);
});
it('should not show a badge if the deployment status is failed', () => {
wrapper = createWrapper({
propsData: { deployment: { ...deployment, pendingApprovalCount: 5, status: 'failed' } },
});
expect(findApprovalBadge().exists()).toBe(false);
});
it('should not show a badge if the deployment does not need approval', () => {
wrapper = createWrapper();

View File

@ -1478,7 +1478,12 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:marked_for_deletion_at) { Time.new(2025, 5, 25) }
let_it_be(:pending_delete_group) do
create(:group_with_deletion_schedule, marked_for_deletion_on: marked_for_deletion_at, developers: user)
end
let_it_be(:pending_delete_project) { create(:project, marked_for_deletion_at: marked_for_deletion_at) }
let_it_be(:parent_pending_delete_project) { create(:project, group: pending_delete_group) }
let(:project_full_path) { pending_delete_project.full_path }
@ -1486,6 +1491,7 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
%(
query {
project(fullPath: "#{project_full_path}") {
markedForDeletion
markedForDeletionOn
permanentDeletionDate
}
@ -1496,16 +1502,22 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
before do
pending_delete_project.add_developer(user)
project.add_developer(user)
stub_application_setting(deletion_adjourned_period: 7)
end
subject(:project_data) do
result = GitlabSchema.execute(query, context: { current_user: user }).as_json
{
marked_for_deletion: result.dig('data', 'project', 'markedForDeletion'),
marked_for_deletion_on: result.dig('data', 'project', 'markedForDeletionOn'),
permanent_deletion_date: result.dig('data', 'project', 'permanentDeletionDate')
}
end
it 'marked_for_deletion returns true' do
expect(project_data[:marked_for_deletion]).to be true
end
it 'marked_for_deletion_on returns correct date' do
marked_for_deletion_on_time = Time.zone.parse(project_data[:marked_for_deletion_on])
@ -1521,9 +1533,21 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj
end
end
context 'when parent is scheduled for deletion' do
let(:project_full_path) { parent_pending_delete_project.full_path }
it 'marked_for_deletion returns true' do
expect(project_data[:marked_for_deletion]).to be true
end
end
context 'when project is not scheduled for deletion' do
let(:project_full_path) { project.full_path }
it 'marked_for_deletion returns false' do
expect(project_data[:marked_for_deletion]).to be false
end
it 'returns theoretical date project will be permanently deleted for permanent_deletion_date' do
expect(project_data[:permanent_deletion_date])
.to eq(::Gitlab::CurrentSettings.deletion_adjourned_period.days.since(Date.current).strftime('%F'))

View File

@ -3,9 +3,11 @@
require 'spec_helper'
RSpec.describe GroupDescendant do
let(:parent) { create(:group) }
let(:subgroup) { create(:group, parent: parent) }
let(:subsub_group) { create(:group, parent: subgroup) }
using RSpec::Parameterized::TableSyntax
let_it_be(:parent) { create(:group) }
let_it_be(:subgroup) { create(:group, parent: parent) }
let_it_be(:subsub_group) { create(:group, parent: subgroup) }
def all_preloaded_groups(*groups)
groups + [parent, subgroup, subsub_group]
@ -46,6 +48,27 @@ RSpec.describe GroupDescendant do
expect { subsub_group.hierarchy(build_stubbed(:group)) }
.to raise_error('specified top is not part of the tree')
end
context 'with upto_preloaded_ancestors_only option' do
where :top, :preloaded, :expected_hierarchy do
nil | [] | ref(:subsub_group)
nil | [ref(:parent)] | ref(:subsub_group)
nil | [ref(:subgroup)] | { ref(:subgroup) => ref(:subsub_group) }
nil | [ref(:parent), ref(:subgroup)] | { ref(:parent) => { ref(:subgroup) => ref(:subsub_group) } }
ref(:parent) | [] | ref(:subsub_group)
ref(:parent) | [ref(:parent)] | ref(:subsub_group)
ref(:parent) | [ref(:subgroup)] | { ref(:subgroup) => ref(:subsub_group) }
ref(:parent) | [ref(:parent), ref(:subgroup)] | { ref(:subgroup) => ref(:subsub_group) }
end
with_them do
subject { subsub_group.hierarchy(top, preloaded, { upto_preloaded_ancestors_only: true }) }
it 'builds hierarchy upto preloaded ancestors only' do
is_expected.to eq(expected_hierarchy)
end
end
end
end
describe '.build_hierarchy' do
@ -95,7 +118,7 @@ RSpec.describe GroupDescendant do
described_class.build_hierarchy([subsub_group])
expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception)
.at_least(:once) do |exception, _|
.at_least(:once) do |exception, _|
expect(exception.backtrace).to be_present
end
end
@ -114,11 +137,24 @@ RSpec.describe GroupDescendant do
expect { described_class.build_hierarchy([subsub_group]) }
.to raise_error(/was not preloaded/)
end
context 'with upto_preloaded_ancestors_only option' do
let_it_be(:other_subgroup) { create(:group, parent: parent) }
let_it_be(:descendants) { [subgroup, other_subgroup, subsub_group] }
subject do
described_class.build_hierarchy(descendants, nil, { upto_preloaded_ancestors_only: true })
end
it "builds descendant's hierarchies with the preloaded ancestors only" do
is_expected.to match_array([{ subgroup => subsub_group }, other_subgroup])
end
end
end
end
context 'for a project' do
let(:project) { create(:project, namespace: subsub_group) }
let_it_be(:project) { create(:project, namespace: subsub_group) }
describe '#hierarchy' do
it 'builds a hierarchy for a project' do
@ -132,6 +168,27 @@ RSpec.describe GroupDescendant do
expect(project.hierarchy(subgroup)).to eq(expected_hierarchy)
end
context 'with upto_preloaded_ancestors_only option' do
where :top, :preloaded, :expected_hierarchy do
nil | [] | ref(:project)
nil | [ref(:subgroup)] | ref(:project)
nil | [ref(:subsub_group)] | { ref(:subsub_group) => ref(:project) }
nil | [ref(:subgroup), ref(:subsub_group)] | { ref(:subgroup) => { ref(:subsub_group) => ref(:project) } }
ref(:subgroup) | [] | ref(:project)
ref(:subgroup) | [ref(:subgroup)] | ref(:project)
ref(:subgroup) | [ref(:subsub_group)] | { ref(:subsub_group) => ref(:project) }
ref(:subgroup) | [ref(:subgroup), ref(:subsub_group)] | { ref(:subsub_group) => ref(:project) }
end
with_them do
subject { project.hierarchy(top, preloaded, { upto_preloaded_ancestors_only: true }) }
it 'builds hierarchy upto preloaded ancestors only' do
is_expected.to eq(expected_hierarchy)
end
end
end
end
describe '.build_hierarchy' do
@ -192,6 +249,19 @@ RSpec.describe GroupDescendant do
expect(actual_hierarchy).to eq(expected_hierarchy)
end
context 'with upto_preloaded_ancestors_only option' do
let_it_be(:other_subgroup) { create(:group, parent: parent) }
let_it_be(:descendants) { [subgroup, other_subgroup, subsub_group, project] }
subject do
described_class.build_hierarchy(descendants, nil, { upto_preloaded_ancestors_only: true })
end
it "builds descendant's hierarchies with the preloaded ancestors only" do
is_expected.to match_array([{ subgroup => { subsub_group => project } }, other_subgroup])
end
end
end
end
end

View File

@ -42,7 +42,7 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
Users::Internal.support_bot_id
end
shared_examples 'work items resolver without N + 1 queries' do
shared_examples 'work items resolver without N + 1 queries' do |threshold: 0|
it 'avoids N+1 queries', :use_sql_query_cache do
post_graphql(query, current_user: current_user) # warm-up
@ -63,7 +63,10 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
author: reporter
)
expect { post_graphql(query, current_user: current_user) }.not_to exceed_all_query_limit(control)
expect do
post_graphql(query, current_user: current_user)
end.not_to exceed_all_query_limit(control).with_threshold(threshold)
expect_graphql_errors_to_be_empty
end
end
@ -79,7 +82,8 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team
describe 'N + 1 queries' do
context 'when querying root fields' do
it_behaves_like 'work items resolver without N + 1 queries'
# Issue to fix N+1 - https://gitlab.com/gitlab-org/gitlab/-/issues/548924
it_behaves_like 'work items resolver without N + 1 queries', threshold: 3
end
# We need a separate example since all_graphql_fields_for will not fetch fields from types

View File

@ -124,9 +124,10 @@ RSpec.describe 'getting a collection of projects', feature_category: :source_cod
# There is an N+1 query related to custom roles - https://gitlab.com/gitlab-org/gitlab/-/issues/515675
# There is an N+1 query for duo_features_enabled cascading setting - https://gitlab.com/gitlab-org/gitlab/-/issues/442164
# There is an N+1 query related to pipelines - https://gitlab.com/gitlab-org/gitlab/-/issues/515677
# There is an N+1 query related to marked_for_deletion - https://gitlab.com/gitlab-org/gitlab/-/issues/548924
expect do
post_graphql(query, current_user: current_user)
end.not_to exceed_all_query_limit(control).with_threshold(8)
end.not_to exceed_all_query_limit(control).with_threshold(12)
end
it 'returns the expected projects' do

View File

@ -49,9 +49,10 @@ RSpec.describe 'find work items by reference', feature_category: :portfolio_mana
extra_work_items = create_list(:work_item, 2, :task, project: project2)
refs = references + extra_work_items.map { |item| item.to_reference(full: true) }
# Issue to fix N+1 - https://gitlab.com/gitlab-org/gitlab/-/issues/548924
expect do
post_graphql(query(refs: refs), current_user: current_user)
end.not_to exceed_all_query_limit(control_count)
end.not_to exceed_all_query_limit(control_count).with_threshold(2)
expect(graphql_data_at('workItemsByReference', 'nodes').size).to eq(3)
end
end