Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-06-27 21:11:58 +00:00
parent 1cfe6678a3
commit 0a56f5aadb
43 changed files with 554 additions and 368 deletions

View File

@ -636,14 +636,11 @@ lib/gitlab/checks/
/doc/administration/license_file.md @lciutacu
/doc/administration/load_balancer.md @axil @eread
/doc/administration/logs/ @axil @eread
/doc/administration/logs/_index.md @lciutacu
/doc/administration/maintenance_mode/ @axil
/doc/administration/merge_request_diffs.md @aqualls
/doc/administration/merge_requests_approvals.md @brendan777
/doc/administration/moderate_users.md @lciutacu
/doc/administration/monitoring/_index.md @lciutacu
/doc/administration/monitoring/github_imports.md @ashrafkhamis
/doc/administration/monitoring/performance/ @lciutacu
/doc/administration/monitoring/prometheus/_index.md @axil @eread
/doc/administration/monitoring/prometheus/registry_exporter.md @z_painter
/doc/administration/nfs.md @axil @eread
@ -665,7 +662,6 @@ lib/gitlab/checks/
/doc/administration/read_only_gitlab.md @axil @eread
/doc/administration/redis/ @axil
/doc/administration/reference_architectures/ @axil @eread
/doc/administration/reply_by_email.md @lciutacu
/doc/administration/reply_by_email_postfix_setup.md @axil @eread
/doc/administration/reporting/ @idurham
/doc/administration/reporting/spamcheck.md @axil @eread
@ -687,7 +683,6 @@ lib/gitlab/checks/
/doc/administration/settings/gitaly_timeouts.md @eread
/doc/administration/settings/import_and_export_settings.md @ashrafkhamis
/doc/administration/settings/import_export_rate_limits.md @ashrafkhamis
/doc/administration/settings/incident_management_rate_limits.md @lciutacu
/doc/administration/settings/instance_template_repository.md @brendan777
/doc/administration/settings/jira_cloud_app.md @msedlakjakubowski
/doc/administration/settings/jira_cloud_app_troubleshooting.md @msedlakjakubowski
@ -725,7 +720,6 @@ lib/gitlab/checks/
/doc/api/access_requests.md @idurham
/doc/api/admin/ @idurham
/doc/api/admin_sidekiq_queues.md @axil @eread
/doc/api/alert_management_alerts.md @lciutacu
/doc/api/api_resources.md @ashrafkhamis
/doc/api/appearance.md @idurham
/doc/api/applications.md @idurham
@ -750,7 +744,6 @@ lib/gitlab/checks/
/doc/api/epic_issues.md @msedlakjakubowski
/doc/api/epic_links.md @msedlakjakubowski
/doc/api/epics.md @msedlakjakubowski
/doc/api/error_tracking.md @lciutacu
/doc/api/external_controls.md @eread
/doc/api/geo_nodes.md @axil
/doc/api/geo_sites.md @axil
@ -1015,7 +1008,6 @@ lib/gitlab/checks/
/doc/integration/snowflake.md @eread
/doc/integration/sourcegraph.md @brendan777
/doc/integration/trello_power_up.md @msedlakjakubowski
/doc/operations/ @lciutacu
/doc/policy/ @axil @eread
/doc/security/ @idurham
/doc/security/asset_proxy.md @msedlakjakubowski
@ -1023,7 +1015,8 @@ lib/gitlab/checks/
/doc/solutions/ @jfullam @Darwinjs @sbrightwell
/doc/solutions/integrations/servicenow.md @ashrafkhamis
/doc/subscriptions/ @lciutacu
/doc/subscriptions/gitlab_com/ @lyspin
/doc/subscriptions/gitlab_com/ @lciutacu
/doc/subscriptions/gitlab_com/compute_minutes.md @lyspin
/doc/subscriptions/gitlab_dedicated/ @lyspin
/doc/topics/ @msedlakjakubowski
/doc/topics/git/ @brendan777
@ -1051,8 +1044,6 @@ lib/gitlab/checks/
/doc/tutorials/left_sidebar/ @sselhorn
/doc/tutorials/merge_requests/ @aqualls
/doc/tutorials/move_personal_project_to_group/ @phillipwells
/doc/tutorials/observability/ @lciutacu
/doc/tutorials/product_analytics_onboarding_website_project/ @lciutacu
/doc/tutorials/protected_workflow/ @aqualls
/doc/tutorials/reviews/ @aqualls
/doc/tutorials/scan_execution_policy/ @rlehmann1
@ -1095,7 +1086,6 @@ lib/gitlab/checks/
/doc/user/emoji_reactions.md @msedlakjakubowski
/doc/user/enterprise_user/ @idurham
/doc/user/get_started/get_started_managing_code.md @brendan777
/doc/user/get_started/get_started_monitoring.md @lciutacu
/doc/user/get_started/get_started_planning_work.md @msedlakjakubowski
/doc/user/get_started/get_started_projects.md @phillipwells
/doc/user/get_started/getting_started_gitlab_duo.md @sselhorn

View File

@ -583,11 +583,16 @@ rspec:artifact-collector ee remainder:
optional: true
- job: rspec-ee system pg16 # 16 jobs
optional: true
- job: rspec fail-fast # 1 job
optional: true
- job: rspec-ee fail-fast # 1 job
optional: true
rules:
- !reference ['.rails:rules:ee-only-migration', rules]
- !reference ['.rails:rules:ee-only-background-migration', rules]
- !reference ['.rails:rules:ee-only-integration', rules]
- !reference ['.rails:rules:ee-only-system', rules]
- !reference ['.rails:rules:rspec fail-fast', rules]
rspec:coverage:
extends:

View File

@ -58,6 +58,20 @@ _quarantine:
# Test jobs
# ------------------------------------------
# ========== Network limiting setup ===========
# Only run a subset of smoke suite
airgapped:
extends:
- .parallel
- .omnibus-e2e
- .with-ignored-runtime-data
variables:
QA_SCENARIO: Test::Instance::Airgapped
QA_RSPEC_TAGS: --tag smoke --tag ~github --tag ~external_api_calls --tag ~skip_live_env
rules:
- !reference [.rules:test:omnibus-base, rules]
- if: $QA_SUITES =~ /Test::Instance::Airgapped/
# Execute smallest test suite to validate omnibus package in dependency update merge request pipelines
health-check:
extends:
@ -250,6 +264,20 @@ registry:
- !reference [.rules:test:omnibus-base, rules]
- if: $QA_SUITES =~ /Test::Integration::Registry/
# ========== Relative url ===========
# Only run a subset of smoke suite
relative-url:
extends:
- .parallel
- .omnibus-e2e
- .with-ignored-runtime-data
variables:
QA_SCENARIO: Test::Instance::RelativeUrl
QA_RSPEC_TAGS: --tag smoke --tag ~orchestrated --tag ~skip_live_env
rules:
- !reference [.rules:test:omnibus-base, rules]
- if: $QA_SUITES =~ /Test::Instance::All/
repository-storage:
extends:
- .omnibus-e2e
@ -259,6 +287,40 @@ repository-storage:
- !reference [.rules:test:omnibus-base, rules]
- if: $QA_SUITES =~ /Test::Instance::RepositoryStorage/
# ========== Object Storage with MiniO ===========
object-storage:
extends:
- .omnibus-e2e
variables:
QA_SCENARIO: Test::Instance::Image
QA_RSPEC_TAGS: --tag object_storage
GITLAB_QA_OPTS: --omnibus-config object_storage
rules:
- !reference [.rules:test:omnibus-base, rules]
- if: $QA_SUITES =~ /Test::Instance::ObjectStorage/
# ========== Object Storage with AWS ===========
object-storage-aws:
extends:
- object-storage
variables:
AWS_S3_ACCESS_KEY: $QA_AWS_S3_ACCESS_KEY
AWS_S3_BUCKET_NAME: $QA_AWS_S3_BUCKET_NAME
AWS_S3_KEY_ID: $QA_AWS_S3_KEY_ID
AWS_S3_REGION: $QA_AWS_S3_REGION
GITLAB_QA_OPTS: --omnibus-config object_storage_aws
# ========== Object Storage with GCS ===========
object-storage-gcs:
extends:
- object-storage
variables:
GCS_BUCKET_NAME: $QA_GCS_BUCKET_NAME
GOOGLE_PROJECT: $QA_GOOGLE_PROJECT
GOOGLE_JSON_KEY: $QA_GOOGLE_JSON_KEY
GOOGLE_CLIENT_EMAIL: $QA_GOOGLE_CLIENT_EMAIL
GITLAB_QA_OPTS: --omnibus-config object_storage_gcs
service-ping-disabled:
extends:
- .omnibus-e2e

View File

@ -83,6 +83,5 @@ Gitlab/NoFindInWorkers:
- 'ee/app/workers/groups/update_repository_storage_worker.rb'
- 'ee/app/workers/namespaces/cascade_duo_features_enabled_worker.rb'
- 'ee/app/workers/namespaces/storage_usage_export_worker.rb'
- 'ee/app/workers/repository_update_mirror_worker.rb'
- 'ee/app/workers/requirements_management/import_requirements_csv_worker.rb'
- 'ee/app/workers/work_items/rolledup_dates/bulk_update_handler.rb'

View File

@ -38,9 +38,7 @@ export default {
}
if (!this.hasMergeRequests) {
return __(
"Merge requests are a place to propose changes you've made to a project and discuss those changes with others",
);
return __('Make a merge request to propose changes to this project.');
}
if (this.isOpenTab) {
@ -55,7 +53,7 @@ export default {
}
if (!this.hasMergeRequests) {
return __('Interested parties can even contribute by pushing commits if they want to.');
return __('Others can contribute by pushing commits to the same branch.');
}
return null;

View File

@ -3,7 +3,6 @@ import { GlCard, GlIcon, GlLink, GlButton, GlToggle, GlAlert } from '@gitlab/ui'
import { s__ } from '~/locale';
import ManageViaMr from '~/vue_shared/security_configuration/components/manage_via_mr.vue';
import SetValidityChecks from '~/security_configuration/graphql/set_validity_checks.graphql';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
export default {
@ -18,8 +17,12 @@ export default {
GlAlert,
ManageViaMr,
},
mixins: [glFeatureFlagsMixin()],
inject: ['projectFullPath', 'validityChecksEnabled', 'validityChecksAvailable'],
inject: [
'projectFullPath',
'userIsProjectAdmin',
'validityChecksEnabled',
'validityChecksAvailable',
],
props: {
feature: {
type: Object,
@ -63,11 +66,12 @@ export default {
showManageViaMr() {
return ManageViaMr.canRender(this.feature);
},
shouldRenderValidityChecks() {
return this.glFeatures.validityChecks;
},
isToggleDisabled() {
return !this.validityChecksAvailable || !this.pipelineSecretDetectionEnabled;
return (
!this.validityChecksAvailable ||
!this.pipelineSecretDetectionEnabled ||
!this.userIsProjectAdmin
);
},
},
methods: {
@ -161,7 +165,7 @@ export default {
</gl-button>
</div>
<div v-if="shouldRenderValidityChecks" class="gl-mt-6" data-testid="validity-checks-section">
<div v-if="validityChecksAvailable" class="gl-mt-6" data-testid="validity-checks-section">
<gl-alert
v-if="shouldShowAlert"
class="gl-mb-5"
@ -195,13 +199,6 @@ export default {
:disabled="isToggleDisabled"
@change="onValidityChecksToggle"
/>
<span class="gl-ml-3 gl-text-sm">
{{
localValidityChecksEnabled
? s__('SecurityConfiguration|Enabled')
: s__('SecurityConfiguration|Not enabled')
}}
</span>
</div>
</div>
</template>

View File

@ -507,15 +507,32 @@ export const getNewWorkItemSharedCache = ({
}
if (widgetName === WIDGET_TYPE_HIERARCHY) {
// Get parent value from shared widget localStorage entry.
const cachedParent = sharedCacheWidgets[WIDGET_TYPE_HIERARCHY]?.parent || null;
// Get parent value from type-specific localStorage entry.
const typeSpecificParent =
workItemTypeSpecificWidgets[WIDGET_TYPE_HIERARCHY]?.parent || null;
// Set fallback parent value
let parent = workItemTypeSpecificWidgets[WIDGET_TYPE_HIERARCHY] ? typeSpecificParent : null;
if (cachedParent) {
// Set parent from cached parent only if it is compatible
// with current work item type, fall back to type-specific parent otherwise.
const allowedParentTypes =
widgetDefinitions.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)
?.allowedParentTypes?.nodes || [];
parent = allowedParentTypes.some((type) => type.id === cachedParent.workItemType.id)
? cachedParent
: typeSpecificParent;
}
widgets.push({
type: 'HIERARCHY',
hasChildren: false,
hasParent: false,
// We're not using `sharedCacheWidgets` for hierarchy parent as
// each work item type can have its own allowed hierarchy parent types.
parent: workItemTypeSpecificWidgets[WIDGET_TYPE_HIERARCHY]
? workItemTypeSpecificWidgets[WIDGET_TYPE_HIERARCHY]?.parent || null
: null,
parent,
depthLimitReachedByType: [],
rolledUpCountsByType: [],
children: {
@ -540,17 +557,57 @@ export const getNewWorkItemSharedCache = ({
}
if (widgetName === WIDGET_TYPE_CUSTOM_FIELDS) {
// Get available custom fields for this work item type
const customFieldsWidgetData = widgetDefinitions.find(
(definition) => definition.type === WIDGET_TYPE_CUSTOM_FIELDS,
);
const availableCustomFieldValues = customFieldsWidgetData.customFieldValues;
// Get custom fields with set values from shared widget localStorage entry.
const cachedCustomFieldValues =
sharedCacheWidgets[WIDGET_TYPE_CUSTOM_FIELDS]?.customFieldValues;
// Get custom fields with set values from type-specific localStorage entry.
const typeSpecificCustomFieldValues =
workItemTypeSpecificWidgets[WIDGET_TYPE_CUSTOM_FIELDS]?.customFieldValues || [];
// Set fallback custom fields value.
let customFieldValues = workItemTypeSpecificWidgets[WIDGET_TYPE_CUSTOM_FIELDS]
? typeSpecificCustomFieldValues
: customFieldsWidgetData?.customFieldValues ?? [];
if (cachedCustomFieldValues && availableCustomFieldValues) {
// Create a merged list of custom fields and its values from shared cache & type-specific cache
customFieldValues = availableCustomFieldValues.map((availableField) => {
const cachedField = cachedCustomFieldValues.find(
(cached) => cached.customField.id === availableField.customField.id,
);
const typeSpecificField = typeSpecificCustomFieldValues.find(
(typeField) => typeField.customField.id === availableField.customField.id,
);
// Grab appropriate field value
let fieldValue = {};
if (cachedField?.selectedOptions || typeSpecificField?.selectedOptions) {
fieldValue = {
selectedOptions: cachedField?.selectedOptions || typeSpecificField?.selectedOptions,
};
} else if (cachedField?.value || typeSpecificField?.value) {
fieldValue = { value: cachedField?.value || typeSpecificField?.value };
}
// Set field value only if present, return empty field otherwise
if (Object.keys(fieldValue).length) {
return {
...availableField,
...fieldValue,
};
}
return { ...availableField };
});
}
widgets.push({
type: WIDGET_TYPE_CUSTOM_FIELDS,
// We're not using `sharedCacheWidgets` for custom fields as
// each work item type can have its own allowed custom fields.
customFieldValues: workItemTypeSpecificWidgets[WIDGET_TYPE_CUSTOM_FIELDS]
? workItemTypeSpecificWidgets[WIDGET_TYPE_CUSTOM_FIELDS]?.customFieldValues || []
: customFieldsWidgetData?.customFieldValues ?? [],
customFieldValues,
__typename: 'WorkItemWidgetCustomFields',
});
}
@ -613,7 +670,7 @@ export const setNewWorkItemCache = async ({
let draftDescription = '';
// Experimental support for shared widget data across work item types
if (gon.features.workItemsAlpha) {
if (gon.features.workItemsBeta) {
const sharedCache = getNewWorkItemSharedCache({
workItemAttributesWrapperOrder,
widgetDefinitions,

View File

@ -191,6 +191,7 @@ class Note < ApplicationRecord
after_commit :trigger_note_subscription_update, on: :update
after_commit :trigger_note_subscription_destroy, on: :destroy
after_commit :broadcast_noteable_notes_changed, unless: :importing?
after_commit :trigger_work_item_updated_subscription, on: :create, if: :system?
def trigger_note_subscription_create
return unless trigger_note_subscription?
@ -221,6 +222,13 @@ class Note < ApplicationRecord
GraphqlTriggers.work_item_note_deleted(noteable.to_work_item_global_id, deleted_note_data)
end
def trigger_work_item_updated_subscription
return unless trigger_note_subscription?
return unless system_note_work_item_reference?
GraphqlTriggers.work_item_updated(noteable)
end
class << self
extend Gitlab::Utils::Override
@ -835,6 +843,10 @@ class Note < ApplicationRecord
def set_internal_flag
self.internal = confidential if confidential
end
def system_note_work_item_reference?
note.present? && system_note_metadata&.about_relation?
end
end
Note.prepend_mod

View File

@ -21,6 +21,19 @@ class SystemNoteMetadata < ApplicationRecord
cloned
].freeze
WORK_ITEMS_CROSS_REFERENCE = %w[
branch
commit
cross_reference
merge
relate
unrelate
unrelate_from_parent
unrelate_from_child
relate_to_parent
relate_to_child
].freeze
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
designs_added designs_modified designs_removed designs_discussion_added
@ -45,6 +58,10 @@ class SystemNoteMetadata < ApplicationRecord
note
end
def about_relation?
action.in?(WORK_ITEMS_CROSS_REFERENCE)
end
def icon_types
ICON_TYPES
end

View File

@ -24,8 +24,7 @@ module Projects
secret_push_protection_available:
Gitlab::CurrentSettings.current_application_settings.secret_push_protection_available,
secret_push_protection_enabled: secret_push_protection_enabled,
validity_checks_available:
Ability.allowed?(current_user, :configure_secret_detection_validity_checks, project),
validity_checks_available: validity_checks_available,
validity_checks_enabled: validity_checks_enabled,
user_is_project_admin: user_is_project_admin?,
secret_detection_configuration_path: secret_detection_configuration_path
@ -105,6 +104,7 @@ module Projects
project.security_setting
end
def validity_checks_available; end
def validity_checks_enabled; end
def container_scanning_for_registry_enabled; end
def secret_push_protection_enabled; end

View File

@ -29,14 +29,13 @@
- else
= render Pajamas::EmptyStateComponent.new(svg_path: 'illustrations/empty-state/empty-merge-requests-md.svg',
empty_state_options: { data: { testid: 'issuable-empty-state' } },
title: _("Merge requests are a place to propose changes you've made to a project and discuss those changes with others")) do |c|
title: _("Make a merge request to propose changes to this project.")) do |c|
- c.with_description do
= _("Interested parties can even contribute by pushing commits if they want to.")
= _("Others can contribute by pushing commits to the same branch.")
- if button_path
.gl-mt-5= link_button_to button_text, button_path,
title: button_text,
id: 'new_merge_request_link',
variant: :confirm,
data: { testid: "new-merge-request-button", **tracking_data }

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module ActionDispatchRoutingRedirectPatch
def build_response(req)
response = super
uri = response.headers['Location'].to_s
body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri)}">redirected</a>.</body></html>)
response.body = body
response.headers["Content-Length"] = body.length.to_s
response
end
end
module ActionControllerRedirectingPatch
def redirect_to(*, **)
super
uri = ERB::Util.unwrapped_html_escape(response.location)
self.response_body = "<html><body>You are being <a href=\"#{uri}\">redirected</a>.</body></html>"
end
end
ActionController::Redirecting.prepend(ActionControllerRedirectingPatch)
ActionDispatch::Routing::Redirect.prepend(ActionDispatchRoutingRedirectPatch)

View File

@ -8,14 +8,6 @@ description: https://docs.gitlab.com/ee/operations/feature_flags.html#feature-fl
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24819
milestone: '12.8'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: strategy_id
table: operations_strategies
sharding_key: project_id
belongs_to: strategy
sharding_key:
project_id: projects
table_size: small
desired_sharding_key_migration_job_name: BackfillOperationsScopesProjectId

View File

@ -8,14 +8,6 @@ description: Join table relating packages_package_files and ci_pipelines
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44348
milestone: '13.6'
gitlab_schema: gitlab_main_cell
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: package_file_id
table: packages_package_files
sharding_key: project_id
belongs_to: package_file
sharding_key:
project_id: projects
table_size: medium
desired_sharding_key_migration_job_name: BackfillPackagesPackageFileBuildInfosProjectId

View File

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

View File

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

View File

@ -0,0 +1 @@
78253d3d6bafd8609d2430f9a16115e044f13be5f50998d567cb09c2ca1673ab

View File

@ -0,0 +1 @@
a0729cad8b034caf95885d7381af5b10530310daf6d7439dacffb0a714c6027e

View File

@ -18925,7 +18925,8 @@ CREATE TABLE operations_scopes (
id bigint NOT NULL,
strategy_id bigint NOT NULL,
environment_scope character varying(255) NOT NULL,
project_id bigint
project_id bigint,
CONSTRAINT check_722a570b84 CHECK ((project_id IS NOT NULL))
);
CREATE SEQUENCE operations_scopes_id_seq
@ -19888,7 +19889,8 @@ CREATE TABLE packages_package_file_build_infos (
id bigint NOT NULL,
package_file_id bigint NOT NULL,
pipeline_id bigint,
project_id bigint
project_id bigint,
CONSTRAINT check_102fc16781 CHECK ((project_id IS NOT NULL))
);
CREATE SEQUENCE packages_package_file_build_infos_id_seq

View File

@ -56,3 +56,22 @@ first attempt to load balance the connection across the replica hosts.
It looks for the next `online` replica host and yields a connection from the host's connection pool.
A replica host is considered `online` if it is up-to-date with the primary, based on
either the replication lag size or time. The thresholds for these requirements are configurable.
## Deployment Strategy
When rolling out changes via feature flag, consider deploying exclusively to Sidekiq pods initially to minimize risk.
Why Sidekiq-first deployment:
- Keeps the API pods stable ensuring ChatOps remains available to disable feature flags in worst case scenario.
- Background jobs can retry automatically without any intervention.
Implementation example:
```ruby
if feature_flag_enabled? && Gitlab::Runtime.sidekiq?
new_changes
else
existing_changes
end
```

View File

@ -58,6 +58,8 @@ Join the conversation about interesting ways to use GitLab O11y in the GitLab O1
## Set up a GitLab Observability instance
Observability data is collected in a separate application outside of your GitLab.com instance. Problems with your GitLab instance do not impact collecting or viewing your observability data and vice-versa.
Prerequisites:
- You must have an EC2 instance or similar virtual machine with:

View File

@ -66,7 +66,7 @@ namespace :tw do
CodeOwnerRule.new('Pipeline Authoring', '@marcel.amirault'),
CodeOwnerRule.new('Pipeline Execution', '@lyspin'),
CodeOwnerRule.new('Pipeline Security', '@marcel.amirault'),
CodeOwnerRule.new('Platform Insights', '@lciutacu'),
# CodeOwnerRule.new('Platform Insights', ''),
CodeOwnerRule.new('Product Planning', '@msedlakjakubowski'),
CodeOwnerRule.new('Project Management', '@msedlakjakubowski'),
CodeOwnerRule.new('Provision', '@lciutacu'),
@ -74,6 +74,7 @@ namespace :tw do
# CodeOwnerRule.new('Respond', ''),
CodeOwnerRule.new('Runner', '@rsarangadharan'),
CodeOwnerRule.new('Hosted Runners', '@rsarangadharan'),
CodeOwnerRule.new('Seat Management', '@lciutacu'),
# CodeOwnerRule.new('Security Infrastructure', ''),
CodeOwnerRule.new('Security Policies', '@rlehmann1'),
CodeOwnerRule.new('Secret Detection', '@phillipwells'),
@ -82,7 +83,7 @@ namespace :tw do
CodeOwnerRule.new('Solutions Architecture', '@jfullam @Darwinjs @sbrightwell'),
CodeOwnerRule.new('Source Code', '@brendan777'),
CodeOwnerRule.new('Static Analysis', '@rdickenson'),
# CodeOwnerRule.new('Subscription Management', ''),
CodeOwnerRule.new('Subscription Management', '@lciutacu'),
CodeOwnerRule.new('Switchboard', '@lyspin'),
CodeOwnerRule.new('Testing', '@eread'),
CodeOwnerRule.new('Tutorials', '@gl-docsteam'),

View File

@ -33533,9 +33533,6 @@ msgstr ""
msgid "Interactive mode"
msgstr ""
msgid "Interested parties can even contribute by pushing commits if they want to."
msgstr ""
msgid "Internal"
msgstr ""
@ -37206,6 +37203,9 @@ msgstr ""
msgid "Maintenance mode"
msgstr ""
msgid "Make a merge request to propose changes to this project."
msgstr ""
msgid "Make adjustments to how your GitLab instance is set up."
msgstr ""
@ -38691,9 +38691,6 @@ msgstr ""
msgid "Merge requests"
msgstr ""
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
msgid "Merge requests awaiting your review."
msgstr ""
@ -43603,6 +43600,9 @@ msgstr ""
msgid "Other visibility settings have been disabled by the administrator."
msgstr ""
msgid "Others can contribute by pushing commits to the same branch."
msgstr ""
msgid "Otherwise, click the link below to complete the process."
msgstr ""

View File

@ -60,10 +60,10 @@
"@gitlab/application-sdk-browser": "^0.3.4",
"@gitlab/at.js": "1.5.7",
"@gitlab/cluster-client": "^3.0.0",
"@gitlab/duo-ui": "^8.22.1",
"@gitlab/duo-ui": "^8.23.0",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.3.0",
"@gitlab/query-language-rust": "0.11.1",
"@gitlab/query-language-rust": "0.11.3",
"@gitlab/svgs": "3.137.0",
"@gitlab/ui": "114.8.1",
"@gitlab/vue-router-vue3": "npm:vue-router@4.5.1",

View File

@ -10,7 +10,8 @@ module QA
tags "~github", "~external_api_calls", "~skip_live_env", *Specs::Runner::DEFAULT_SKIPPED_TAGS
pipeline_mappings test_on_omnibus_nightly: ["airgapped"]
pipeline_mappings test_on_omnibus: %w[airgapped],
test_on_omnibus_nightly: %w[airgapped]
def perform(address, *rspec_options)
Runtime::Scenario.define(:network, 'airgapped')

View File

@ -14,7 +14,7 @@ module QA
pipeline_mappings test_on_cng: %w[cng-instance],
test_on_gdk: %w[gdk-instance gdk-instance-gitaly-transactions gdk-instance-ff-inverse],
test_on_omnibus: %w[instance git-sha256-repositories],
test_on_omnibus: %w[instance git-sha256-repositories relative-url],
test_on_omnibus_nightly: %w[
instance-image-slow-network
nplus1-instance-image

View File

@ -7,7 +7,8 @@ module QA
class ObjectStorage < All
tags :object_storage
pipeline_mappings test_on_omnibus_nightly: %w[object-storage object-storage-aws object-storage-gcs]
pipeline_mappings test_on_omnibus: %w[object-storage object-storage-aws object-storage-gcs],
test_on_omnibus_nightly: %w[object-storage object-storage-aws object-storage-gcs]
end
end
end

View File

@ -11,7 +11,6 @@ module QA
# @return [Array] scenarios that never run in test-on-omnibus pipeline
IGNORED_SCENARIOS = [
"QA::EE::Scenario::Test::Geo",
"QA::Scenario::Test::Instance::Airgapped",
"QA::Scenario::Test::Sanity::Selectors"
].freeze

View File

@ -456,6 +456,7 @@ RSpec.describe ProjectsController, feature_category: :groups_and_projects do
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(project_path(public_project, ref: 'master', path: '/.gitlab-ci.yml'))
expect(response.body).to match(/You.are.being.+redirected/)
end
end

View File

@ -33,10 +33,10 @@ describe('Merge request list app empty state component', () => {
createComponent({ hasMergeRequests: false });
expect(findEmptyState().attributes('title')).toBe(
"Merge requests are a place to propose changes you've made to a project and discuss those changes with others",
'Make a merge request to propose changes to this project.',
);
expect(findEmptyState().attributes('description')).toBe(
'Interested parties can even contribute by pushing commits if they want to.',
'Others can contribute by pushing commits to the same branch.',
);
});
});

View File

@ -44,7 +44,6 @@ describe('PipelineSecretDetectionFeatureCard component', () => {
provide: {
projectFullPath: 'group/project',
userIsProjectAdmin: true,
glFeatures: { validityChecks: true },
validityChecksEnabled: false,
validityChecksAvailable: true,
...provide,
@ -240,145 +239,151 @@ describe('PipelineSecretDetectionFeatureCard component', () => {
});
describe('validity checks section', () => {
beforeEach(() => {
feature = makeFeature({ available: true });
});
it.each`
validityChecksAvailable | shouldRender
${true} | ${true}
${false} | ${false}
`(
'should render $shouldRender when validityChecksAvailable=$validityChecksAvailable',
({ validityChecksAvailable, shouldRender }) => {
feature = makeFeature({ available: true });
createComponent({}, { validityChecksAvailable });
it('is shown when feature flag is enabled', () => {
createComponent({}, { glFeatures: { validityChecks: true } });
expect(findValidityChecksSection().exists()).toBe(true);
});
expect(findValidityChecksSection().exists()).toBe(shouldRender);
},
);
it('is not shown when feature flag is disabled', () => {
createComponent({}, { glFeatures: { validityChecks: false } });
expect(findValidityChecksSection().exists()).toBe(false);
});
it('is not shown when feature is unavailable', () => {
feature = makeFeature({ available: false });
createComponent({}, { glFeatures: { validityChecks: true } });
expect(findValidityChecksSection().exists()).toBe(false);
});
describe('validity checks toggle', () => {
it('has the correct default value when validityChecksEnabled is true', () => {
createComponent({}, { validityChecksEnabled: true });
const toggle = findValidityChecksToggle();
expect(toggle.props('value')).toBe(true);
});
it('has the correct default value when validityChecksEnabled is false', () => {
createComponent({}, { validityChecksEnabled: false });
const toggle = findValidityChecksToggle();
expect(toggle.props('value')).toBe(false);
});
it('is unlocked when validityChecksAvailable is true and pipeline secret detection is configured', () => {
feature = makeFeature({ available: true, configured: true });
createComponent({}, { validityChecksAvailable: true });
const toggle = findValidityChecksToggle();
expect(toggle.props('disabled')).toBe(false);
});
it('is locked when validityChecksAvailable is false', () => {
feature = makeFeature({ available: true, configured: true });
createComponent({}, { validityChecksAvailable: false });
const toggle = findValidityChecksToggle();
expect(toggle.props('disabled')).toBe(true);
});
it('is locked when pipeline secret detection is not configured', () => {
feature = makeFeature({ available: true, configured: false });
createComponent({}, { validityChecksAvailable: true });
const toggle = findValidityChecksToggle();
expect(toggle.props('disabled')).toBe(true);
});
it('calls mutation on toggle change with correct payload', async () => {
feature = makeFeature({ available: true, configured: true });
createComponent();
const toggle = findValidityChecksToggle();
expect(toggle.props('value')).toBe(false);
toggle.vm.$emit('change', true);
expect(requestHandlers.setMutationHandler).toHaveBeenCalledWith({
input: {
namespacePath: 'group/project',
enable: true,
},
});
await waitForPromises();
expect(toggle.props('value')).toBe(true);
expect(wrapper.text()).toContain('Enabled');
});
it('shows success toast when toggle succeeds', async () => {
feature = makeFeature({ available: true, configured: true });
createComponent();
const toggle = findValidityChecksToggle();
expect(toggle.props('value')).toBe(false);
toggle.vm.$emit('change', true);
expect(requestHandlers.setMutationHandler).toHaveBeenCalledWith({
input: {
namespacePath: 'group/project',
enable: true,
},
});
await waitForPromises();
expect(toggle.props('value')).toBe(true);
expect(mockToastShow).toHaveBeenCalledWith('Validity checks enabled');
});
it('shows error alert when an error message is set', async () => {
feature = makeFeature({ available: true, configured: true });
createComponent();
requestHandlers.setMutationHandler.mockReset();
requestHandlers.setMutationHandler.mockResolvedValue({
data: {
setValidityChecks: {
validityChecksEnabled: null,
errors: ['data response with errors'],
describe('toggle state', () => {
it.each`
available | configured | userIsProjectAdmin | shouldBeDisabled
${true} | ${true} | ${true} | ${false}
${true} | ${false} | ${true} | ${true}
${true} | ${true} | ${false} | ${true}
${true} | ${false} | ${false} | ${true}
`(
'disabled=$shouldBeDisabled when available=$available, configured=$configured, userIsProjectAdmin=$userIsProjectAdmin',
({ available, configured, userIsProjectAdmin, shouldBeDisabled }) => {
feature = makeFeature({ available, configured });
createComponent(
{},
{
validityChecksAvailable: true,
userIsProjectAdmin,
},
},
});
);
const toggle = findValidityChecksToggle();
toggle.vm.$emit('change', true);
expect(findValidityChecksToggle().props('disabled')).toBe(shouldBeDisabled);
},
);
});
await waitForPromises();
describe('toggle value', () => {
it.each`
validityChecksEnabled | expectedValue
${true} | ${true}
${false} | ${false}
`(
'value is $expectedValue when validityChecksEnabled=$validityChecksEnabled',
({ validityChecksEnabled, expectedValue }) => {
feature = makeFeature({ available: true, configured: true });
createComponent(
{},
{
validityChecksAvailable: true,
validityChecksEnabled,
userIsProjectAdmin: true,
},
);
expect(findValidityChecksAlert().exists()).toBe(true);
expect(findValidityChecksToggle().props('value')).toBe(expectedValue);
},
);
});
it('calls mutation on toggle change with correct payload', async () => {
feature = makeFeature({ available: true, configured: true });
createComponent();
const toggle = findValidityChecksToggle();
expect(toggle.props('value')).toBe(false);
toggle.vm.$emit('change', true);
expect(requestHandlers.setMutationHandler).toHaveBeenCalledWith({
input: {
namespacePath: 'group/project',
enable: true,
},
});
it('handles GraphQL mutation errors', async () => {
feature = makeFeature({ available: true, configured: true });
createComponent();
await waitForPromises();
requestHandlers.setMutationHandler.mockReset();
requestHandlers.setMutationHandler.mockRejectedValue(new Error('Network error'));
expect(toggle.props('value')).toBe(true);
expect(wrapper.text()).toContain('Enabled');
});
const toggle = findValidityChecksToggle();
toggle.vm.$emit('change', true);
it('shows success toast when toggle succeeds', async () => {
feature = makeFeature({ available: true, configured: true });
createComponent();
expect(requestHandlers.setMutationHandler).toHaveBeenCalledWith({
input: {
namespacePath: 'group/project',
enable: true,
},
});
const toggle = findValidityChecksToggle();
expect(toggle.props('value')).toBe(false);
await waitForPromises();
toggle.vm.$emit('change', true);
expect(findValidityChecksAlert().exists()).toBe(true);
expect(requestHandlers.setMutationHandler).toHaveBeenCalledWith({
input: {
namespacePath: 'group/project',
enable: true,
},
});
await waitForPromises();
expect(toggle.props('value')).toBe(true);
expect(mockToastShow).toHaveBeenCalledWith('Validity checks enabled');
});
it('shows error alert when an error message is set', async () => {
feature = makeFeature({ available: true, configured: true });
createComponent();
requestHandlers.setMutationHandler.mockReset();
requestHandlers.setMutationHandler.mockResolvedValue({
data: {
setValidityChecks: {
validityChecksEnabled: null,
errors: ['data response with errors'],
},
},
});
const toggle = findValidityChecksToggle();
toggle.vm.$emit('change', true);
await waitForPromises();
expect(findValidityChecksAlert().exists()).toBe(true);
});
it('handles GraphQL mutation errors', async () => {
feature = makeFeature({ available: true, configured: true });
createComponent();
requestHandlers.setMutationHandler.mockReset();
requestHandlers.setMutationHandler.mockRejectedValue(new Error('Network error'));
const toggle = findValidityChecksToggle();
toggle.vm.$emit('change', true);
expect(requestHandlers.setMutationHandler).toHaveBeenCalledWith({
input: {
namespacePath: 'group/project',
enable: true,
},
});
await waitForPromises();
expect(findValidityChecksAlert().exists()).toBe(true);
});
});

View File

@ -8,7 +8,7 @@ import {
updateCacheAfterCreatingNote,
updateCountsForParent,
} from '~/work_items/graphql/cache_utils';
import { findHierarchyWidget, findNotesWidget, getWorkItemWidgets } from '~/work_items/utils';
import { findHierarchyWidget, findNotesWidget } from '~/work_items/utils';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { apolloProvider } from '~/graphql_shared/issuable_client';
@ -20,9 +20,7 @@ import {
workItemResponseFactory,
mockCreateWorkItemDraftData,
mockNewWorkItemCache,
mockNewWorkItemIssueCache,
restoredDraftDataWidgets,
restoredDraftDataWidgetsForIssue,
restoredDraftDataWidgetsEmpty,
} from '../mock_data';
@ -50,10 +48,6 @@ describe('work items graphql cache utils', () => {
],
},
};
// This looks like an odd pattern but is something we do already in several places
// across our codebase to run tests conditionally, often to quarantine tests.
// Here we're utilizing it skip test based on feature flag.
const itif = (condition) => (condition ? it : it.skip);
beforeEach(() => {
window.gon.features = {};
@ -268,13 +262,6 @@ describe('work items graphql cache utils', () => {
`autosave/new-gitlab-org-epic-draft`,
JSON.stringify(mockCreateWorkItemDraftData),
);
if (window.gon.features.workItemsAlpha) {
localStorage.setItem(
`autosave/new-gitlab-org-widgets-draft`,
JSON.stringify(getWorkItemWidgets(mockCreateWorkItemDraftData)),
);
}
});
afterEach(() => {
@ -326,27 +313,6 @@ describe('work items graphql cache utils', () => {
);
},
);
itif(workItemsAlpha)('shares widget data between work item types', async () => {
await setNewWorkItemCache(mockNewWorkItemIssueCache);
await waitForPromises();
expect(mockWriteQuery).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
workspace: expect.objectContaining({
workItem: expect.objectContaining({
// The title was originally set for Epic type in beforeEach call above
title: mockCreateWorkItemDraftData.workspace.workItem.title,
// The widgets data is shared
widgets: expect.arrayContaining(restoredDraftDataWidgetsForIssue),
}),
}),
}),
}),
);
});
},
);

View File

@ -7248,88 +7248,6 @@ export const mockNewWorkItemCache = {
workItemTypeIconName: 'issue-type-epic',
};
export const mockNewWorkItemIssueCache = {
fullPath: 'gitlab-org',
widgetDefinitions: [
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'AWARD_EMOJI',
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'CURRENT_USER_TODOS',
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'DESCRIPTION',
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'HEALTH_STATUS',
},
{
__typename: 'WorkItemWidgetDefinitionHierarchy',
type: 'HIERARCHY',
allowedChildTypes: {
__typename: 'WorkItemTypeConnection',
nodes: [
{
__typename: 'WorkItemType',
id: 'gid://gitlab/WorkItems::Type/5',
name: 'Task',
},
],
},
},
{
__typename: 'WorkItemWidgetDefinitionLabels',
type: 'LABELS',
allowsScopedLabels: true,
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'LINKED_ITEMS',
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'NOTES',
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'NOTIFICATIONS',
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'PARTICIPANTS',
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'START_AND_DUE_DATE',
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'STATUS',
},
{
__typename: 'WorkItemWidgetDefinitionGeneric',
type: 'TIME_TRACKING',
},
{
__typename: 'WorkItemWidgetDefinitionWeight',
type: 'WEIGHT',
editable: false,
rollUp: true,
},
{
__typename: 'WorkItemWidgetDefinitionCustomFields',
type: WIDGET_TYPE_CUSTOM_FIELDS,
},
],
workItemType: 'Issue',
workItemTypeId: 'gid://gitlab/WorkItems::Type/2',
workItemTypeIconName: 'issue-type-issue',
};
export const restoredDraftDataWidgets = [
{
type: 'DESCRIPTION',
@ -7450,29 +7368,6 @@ export const restoredDraftDataWidgets = [
},
];
export const restoredDraftDataWidgetsForIssue = restoredDraftDataWidgets
// Drop any unsupported widget for Issue type
.filter((widget) => !['COLOR'].includes(widget.type))
// Override specific widgets for Issue type
.map((widget) => {
if (widget.type === 'HIERARCHY') {
return {
type: 'HIERARCHY',
hasChildren: false,
hasParent: false,
parent: null,
depthLimitReachedByType: [],
rolledUpCountsByType: [],
children: {
nodes: [],
__typename: 'WorkItemConnection',
},
__typename: 'WorkItemWidgetHierarchy',
};
}
return { ...widget };
});
export const restoredDraftDataWidgetsEmpty = [
{
type: 'DESCRIPTION',

View File

@ -2118,4 +2118,62 @@ RSpec.describe Note, feature_category: :team_planning do
end
end
end
describe '#trigger_work_item_updated_subscription' do
let(:issue) { create(:issue) }
let(:note) { build(:note, noteable: issue, system: true, project: issue.project) }
before do
allow(note).to receive(:for_issue?).and_return(true)
end
context 'when note contains metadata with specific action' do
%w[branch relate cross_reference].each do |action|
it "triggers subscription for note with '#{action}' action" do
build(:system_note_metadata, note: note, action: action)
expect(GraphqlTriggers).to receive(:work_item_updated).with(issue)
note.save!
end
end
end
context 'when note contains metadata with non-actionable action' do
%w[label visible assignee].each do |action|
it "does not trigger subscription for note with '#{action}' action" do
build(:system_note_metadata, note: note, action: action)
expect(GraphqlTriggers).not_to receive(:work_item_updated)
note.save!
end
end
end
context 'when noteable is not an issue' do
let(:merge_request) { create(:merge_request) }
let(:note) { build(:note, noteable: merge_request, system: true, note: 'merge request created', project: merge_request.project) }
before do
allow(note).to receive(:for_issue?).and_return(false)
end
it 'does not trigger subscription' do
expect(GraphqlTriggers).not_to receive(:work_item_updated)
note.save!
end
end
context 'when noteable is not a system note' do
let(:note) { build(:note, noteable: issue, system: false, note: 'merge request created', project: issue.project) }
it 'does not trigger subscription' do
expect(GraphqlTriggers).not_to receive(:work_item_updated)
note.save!
end
end
end
end

View File

@ -49,4 +49,39 @@ RSpec.describe SystemNoteMetadata, feature_category: :team_planning do
it { expect(described_class.for_notes(::Note.id_in(notes))).to match_array([metadata1, metadata2]) }
end
end
describe '#about_relation?' do
let(:note) { create(:note) }
let(:system_note_metadata) { build(:system_note_metadata, note: note) }
context 'when action is in cross_reference_types_with_branch' do
SystemNoteMetadata::WORK_ITEMS_CROSS_REFERENCE.each do |action_type|
it "returns true for action '#{action_type}'" do
system_note_metadata.action = action_type
expect(system_note_metadata.about_relation?).to be true
end
end
end
context 'when action is not in cross_reference_types_with_branch' do
let(:non_cross_reference_actions) do
SystemNoteMetadata::ICON_TYPES - SystemNoteMetadata::WORK_ITEMS_CROSS_REFERENCE
end
it 'returns false for actions not in cross reference types' do
non_cross_reference_actions.each do |action_type|
system_note_metadata.action = action_type
expect(system_note_metadata.about_relation?).to be false
end
end
it 'returns false for custom action not in any predefined types' do
system_note_metadata.action = 'custom_action'
expect(system_note_metadata.about_relation?).to be false
end
end
end
end

View File

@ -166,6 +166,7 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{repository_path}.git/info/refs")
expect(response.body).to match(/You.are.being.+redirected/)
end
end
@ -178,6 +179,7 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{repository_path}.git/info/refs?service=#{params[:service]}")
expect(response.body).to match(/You.are.being.+redirected/)
end
end
@ -190,6 +192,7 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{repository_path}.git/info/refs?service=#{params[:service]}")
expect(response.body).to match(/You.are.being.+redirected/)
end
end
@ -202,6 +205,7 @@ RSpec.describe 'Git HTTP requests', feature_category: :source_code_management do
it "redirects to the sign-in page" do
expect(response).to redirect_to(new_user_session_path)
expect(response.body).to match(/You.are.being.+redirected/)
end
end
end

View File

@ -1171,6 +1171,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
it 'triggers a workItemUpdated subscription for all affected records' do
service = described_class.new(project: project, current_user: user, params: update_params)
allow(service).to receive(:execute_hooks)
allow(GraphqlTriggers).to receive(:work_item_updated).and_call_original
WorkItem.where(id: issues_to_notify).find_each do |work_item|
expect(GraphqlTriggers).to receive(:work_item_updated).with(work_item).once.and_call_original

View File

@ -111,7 +111,7 @@ RSpec.describe WorkItems::ParentLinks::CreateService, feature_category: :portfol
let(:params) { { issuable_references: [task1, task2] } }
it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do
let(:trigger_call_counter) { 2 }
let(:trigger_call_counter) { 4 }
subject(:execute_service) { described_class.new(work_item, user, params).execute }
end
@ -242,6 +242,8 @@ RSpec.describe WorkItems::ParentLinks::CreateService, feature_category: :portfol
it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do
subject(:execute_service) { described_class.new(work_item, user, params).execute }
let(:trigger_call_counter) { 2 }
end
it 'creates links only for non related tasks', :aggregate_failures do

View File

@ -30,6 +30,8 @@ RSpec.describe WorkItems::ParentLinks::DestroyService, feature_category: :team_p
it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do
subject(:execute_service) { described_class.new(parent_link, user).execute }
let(:trigger_call_counter) { 2 }
end
it 'removes relation and creates notes', :aggregate_failures do

View File

@ -66,6 +66,7 @@ RSpec.describe WorkItems::ParentLinks::ReorderService, feature_category: :portfo
it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do
let(:update_subject) { parent }
let(:execute_service) { subject }
let(:trigger_call_counter) { call_counter_nested }
end
end
@ -94,7 +95,9 @@ RSpec.describe WorkItems::ParentLinks::ReorderService, feature_category: :portfo
let(:base_param) { { target_issuable: work_item } }
shared_examples 'updates hierarchy order without notes' do
it_behaves_like 'processes ordered hierarchy'
it_behaves_like 'processes ordered hierarchy' do
let(:call_counter_nested) { 1 }
end
it 'keeps relationships', :aggregate_failures do
expect { subject }.to not_change { parent_link_class.count }
@ -123,7 +126,9 @@ RSpec.describe WorkItems::ParentLinks::ReorderService, feature_category: :portfo
context 'when new parent is assigned' do
shared_examples 'updates hierarchy order and creates notes' do
it_behaves_like 'processes ordered hierarchy'
it_behaves_like 'processes ordered hierarchy' do
let(:call_counter_nested) { call_counter }
end
it 'creates notes', :aggregate_failures do
subject
@ -139,13 +144,17 @@ RSpec.describe WorkItems::ParentLinks::ReorderService, feature_category: :portfo
context 'when moving before adjacent work item' do
let(:params) { base_param.merge({ adjacent_work_item: last_adjacent, relative_position: 'BEFORE' }) }
it_behaves_like 'updates hierarchy order and creates notes'
it_behaves_like 'updates hierarchy order and creates notes' do
let(:call_counter) { 2 }
end
end
context 'when moving after adjacent work item' do
let(:params) { base_param.merge({ adjacent_work_item: top_adjacent, relative_position: 'AFTER' }) }
it_behaves_like 'updates hierarchy order and creates notes'
it_behaves_like 'updates hierarchy order and creates notes' do
let(:call_counter) { 2 }
end
end
context 'when previous parent was in place' do
@ -157,13 +166,17 @@ RSpec.describe WorkItems::ParentLinks::ReorderService, feature_category: :portfo
context 'when moving before adjacent work item' do
let(:params) { base_param.merge({ adjacent_work_item: last_adjacent, relative_position: 'BEFORE' }) }
it_behaves_like 'updates hierarchy order and creates notes'
it_behaves_like 'updates hierarchy order and creates notes' do
let(:call_counter) { 2 }
end
end
context 'when moving after adjacent work item' do
let(:params) { base_param.merge({ adjacent_work_item: top_adjacent, relative_position: 'AFTER' }) }
it_behaves_like 'updates hierarchy order and creates notes'
it_behaves_like 'updates hierarchy order and creates notes' do
let(:call_counter) { 2 }
end
end
end
end

View File

@ -494,7 +494,7 @@ RSpec.describe WorkItems::UpdateService, feature_category: :team_planning do
end
it_behaves_like 'update service that triggers GraphQL work_item_updated subscription' do
let(:trigger_call_counter) { 2 }
let(:trigger_call_counter) { 3 }
subject(:execute_service) { update_work_item }
end

View File

@ -12,6 +12,8 @@ RSpec.shared_examples 'update service that triggers GraphQL work_item_updated su
let(:trigger_call_counter) { 1 }
it 'triggers graphql subscription workItemUpdated' do
allow(GraphqlTriggers).to receive(:work_item_updated).and_call_original
expect(GraphqlTriggers)
.to receive(:work_item_updated)
.with(update_subject)

View File

@ -1389,10 +1389,10 @@
core-js "^3.29.1"
mitt "^3.0.1"
"@gitlab/duo-ui@^8.22.1":
version "8.22.1"
resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-8.22.1.tgz#0c20839d23b54f8734cde09448b5fa0a95d654b1"
integrity sha512-Qfsmf5YXRnP48B8d7E98NMZO6wQF3BqgtULAOqa9i/0BGAn9aKBNLVLoSid+pDvwu3DzpHDFvh8+MFL6vnb3Fw==
"@gitlab/duo-ui@^8.23.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-8.23.0.tgz#eedfa98fa902f52b6cf7970399528f5d876d7371"
integrity sha512-QOurIDNzSrgRg40omKvdQbhHbuBuFc5dweH2NZjpXjj/hksWGWJntUuzF6LsksucBJ912hQ8Z34+i57BNBQO8g==
dependencies:
"@floating-ui/dom" "1.7.1"
echarts "^5.3.2"
@ -1438,10 +1438,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454"
integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q==
"@gitlab/query-language-rust@0.11.1":
version "0.11.1"
resolved "https://registry.yarnpkg.com/@gitlab/query-language-rust/-/query-language-rust-0.11.1.tgz#8ba31bf1469da1fc2b5c8dc2578bb60725d21e88"
integrity sha512-JlVXPM6dccc2EwoYB8EHo4Z/vjrGVZ//4VTpp5mgWnLvJLW8cEjIKBl0rbFOWVLRIMdlVVtYrQJH1MLugyDhQg==
"@gitlab/query-language-rust@0.11.3":
version "0.11.3"
resolved "https://registry.yarnpkg.com/@gitlab/query-language-rust/-/query-language-rust-0.11.3.tgz#0ced6816989a8d8a61d2326aa14afbc120fdd883"
integrity sha512-wyhZuUj5m2mmM2oxcQnMrgYu1GaGx6LctE/MMnk4Nc878AcKMiJEXknw5vNotO/XXHMFv+YHJm22uTF1ag2qHw==
"@gitlab/stylelint-config@6.2.2":
version "6.2.2"