diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index 8487494d589..9a3ca49b7d1 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -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
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index c3c65b6ea5d..dcdb6dabed3 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -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:
diff --git a/.gitlab/ci/test-on-omnibus/main.gitlab-ci.yml b/.gitlab/ci/test-on-omnibus/main.gitlab-ci.yml
index 877214dbb83..9bfc748c8d1 100644
--- a/.gitlab/ci/test-on-omnibus/main.gitlab-ci.yml
+++ b/.gitlab/ci/test-on-omnibus/main.gitlab-ci.yml
@@ -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
diff --git a/.rubocop_todo/gitlab/no_find_in_workers.yml b/.rubocop_todo/gitlab/no_find_in_workers.yml
index 90995a155a6..44a568dddf5 100644
--- a/.rubocop_todo/gitlab/no_find_in_workers.yml
+++ b/.rubocop_todo/gitlab/no_find_in_workers.yml
@@ -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'
diff --git a/app/assets/javascripts/merge_requests/list/components/empty_state.vue b/app/assets/javascripts/merge_requests/list/components/empty_state.vue
index 51a487c627b..06fe80ac362 100644
--- a/app/assets/javascripts/merge_requests/list/components/empty_state.vue
+++ b/app/assets/javascripts/merge_requests/list/components/empty_state.vue
@@ -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;
diff --git a/app/assets/javascripts/security_configuration/components/pipeline_secret_detection_feature_card.vue b/app/assets/javascripts/security_configuration/components/pipeline_secret_detection_feature_card.vue
index 46cbfa1ba50..f562d83dff6 100644
--- a/app/assets/javascripts/security_configuration/components/pipeline_secret_detection_feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/pipeline_secret_detection_feature_card.vue
@@ -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 {
-
+
-
- {{
- localValidityChecksEnabled
- ? s__('SecurityConfiguration|Enabled')
- : s__('SecurityConfiguration|Not enabled')
- }}
-
diff --git a/app/assets/javascripts/work_items/graphql/cache_utils.js b/app/assets/javascripts/work_items/graphql/cache_utils.js
index 29b7d189c54..c83f1341970 100644
--- a/app/assets/javascripts/work_items/graphql/cache_utils.js
+++ b/app/assets/javascripts/work_items/graphql/cache_utils.js
@@ -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,
diff --git a/app/models/note.rb b/app/models/note.rb
index 186b20f3441..17697da2e99 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -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
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 3b2ac96ae5d..e3d0fef546c 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -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
diff --git a/app/presenters/projects/security/configuration_presenter.rb b/app/presenters/projects/security/configuration_presenter.rb
index abfc1e4a684..69cb920f31f 100644
--- a/app/presenters/projects/security/configuration_presenter.rb
+++ b/app/presenters/projects/security/configuration_presenter.rb
@@ -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
diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml
index 51b96b40897..1155582e080 100644
--- a/app/views/shared/empty_states/_merge_requests.html.haml
+++ b/app/views/shared/empty_states/_merge_requests.html.haml
@@ -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 }
-
diff --git a/config/initializers/rails_redirection_patches.rb b/config/initializers/rails_redirection_patches.rb
new file mode 100644
index 00000000000..2e3bfa140f4
--- /dev/null
+++ b/config/initializers/rails_redirection_patches.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module ActionDispatchRoutingRedirectPatch
+ def build_response(req)
+ response = super
+
+ uri = response.headers['Location'].to_s
+ body = %(You are being redirected.)
+
+ 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 = "You are being redirected."
+ end
+end
+
+ActionController::Redirecting.prepend(ActionControllerRedirectingPatch)
+ActionDispatch::Routing::Redirect.prepend(ActionDispatchRoutingRedirectPatch)
diff --git a/db/docs/operations_scopes.yml b/db/docs/operations_scopes.yml
index 578917479e1..e6617375a68 100644
--- a/db/docs/operations_scopes.yml
+++ b/db/docs/operations_scopes.yml
@@ -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
diff --git a/db/docs/packages_package_file_build_infos.yml b/db/docs/packages_package_file_build_infos.yml
index f44c2bbeef8..bc3c420fce5 100644
--- a/db/docs/packages_package_file_build_infos.yml
+++ b/db/docs/packages_package_file_build_infos.yml
@@ -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
diff --git a/db/post_migrate/20250626154356_add_packages_package_file_build_infos_project_id_not_null.rb b/db/post_migrate/20250626154356_add_packages_package_file_build_infos_project_id_not_null.rb
new file mode 100644
index 00000000000..8d394699fe1
--- /dev/null
+++ b/db/post_migrate/20250626154356_add_packages_package_file_build_infos_project_id_not_null.rb
@@ -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
diff --git a/db/post_migrate/20250626155513_add_operations_scopes_project_id_not_null.rb b/db/post_migrate/20250626155513_add_operations_scopes_project_id_not_null.rb
new file mode 100644
index 00000000000..ebc6a88e614
--- /dev/null
+++ b/db/post_migrate/20250626155513_add_operations_scopes_project_id_not_null.rb
@@ -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
diff --git a/db/schema_migrations/20250626154356 b/db/schema_migrations/20250626154356
new file mode 100644
index 00000000000..297915eb6d1
--- /dev/null
+++ b/db/schema_migrations/20250626154356
@@ -0,0 +1 @@
+78253d3d6bafd8609d2430f9a16115e044f13be5f50998d567cb09c2ca1673ab
\ No newline at end of file
diff --git a/db/schema_migrations/20250626155513 b/db/schema_migrations/20250626155513
new file mode 100644
index 00000000000..d8f6f39596a
--- /dev/null
+++ b/db/schema_migrations/20250626155513
@@ -0,0 +1 @@
+a0729cad8b034caf95885d7381af5b10530310daf6d7439dacffb0a714c6027e
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index afecf53a593..d84cc53ea62 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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
diff --git a/doc/development/database/load_balancing.md b/doc/development/database/load_balancing.md
index a53609c930e..5cf0441586f 100644
--- a/doc/development/database/load_balancing.md
+++ b/doc/development/database/load_balancing.md
@@ -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
+```
diff --git a/doc/operations/observability.md b/doc/operations/observability.md
index 497917ae48e..24a0a6fa752 100644
--- a/doc/operations/observability.md
+++ b/doc/operations/observability.md
@@ -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:
diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake
index f140ff0e530..ec02d7fc6a0 100644
--- a/lib/tasks/gitlab/tw/codeowners.rake
+++ b/lib/tasks/gitlab/tw/codeowners.rake
@@ -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'),
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9e0345d1ccd..514486df81a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/package.json b/package.json
index 5634c533e04..f5a8aaf1a1a 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/qa/qa/scenario/test/instance/airgapped.rb b/qa/qa/scenario/test/instance/airgapped.rb
index 57b4696acf5..3b1bac56d56 100644
--- a/qa/qa/scenario/test/instance/airgapped.rb
+++ b/qa/qa/scenario/test/instance/airgapped.rb
@@ -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')
diff --git a/qa/qa/scenario/test/instance/all.rb b/qa/qa/scenario/test/instance/all.rb
index b3c0af42e5b..04f09d5c3de 100644
--- a/qa/qa/scenario/test/instance/all.rb
+++ b/qa/qa/scenario/test/instance/all.rb
@@ -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
diff --git a/qa/qa/scenario/test/instance/object_storage.rb b/qa/qa/scenario/test/instance/object_storage.rb
index 83e6b012fae..ddcafe26cb9 100644
--- a/qa/qa/scenario/test/instance/object_storage.rb
+++ b/qa/qa/scenario/test/instance/object_storage.rb
@@ -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
diff --git a/qa/qa/tools/ci/scenario_examples.rb b/qa/qa/tools/ci/scenario_examples.rb
index 3ea851c666a..9ad03ec90f6 100644
--- a/qa/qa/tools/ci/scenario_examples.rb
+++ b/qa/qa/tools/ci/scenario_examples.rb
@@ -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
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 23a00b8eea3..6f813864cf9 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -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
diff --git a/spec/frontend/merge_requests/list/components/empty_state_spec.js b/spec/frontend/merge_requests/list/components/empty_state_spec.js
index a292d6a7e8d..d8b1a952f93 100644
--- a/spec/frontend/merge_requests/list/components/empty_state_spec.js
+++ b/spec/frontend/merge_requests/list/components/empty_state_spec.js
@@ -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.',
);
});
});
diff --git a/spec/frontend/security_configuration/components/pipeline_secret_detection_feature_card_spec.js b/spec/frontend/security_configuration/components/pipeline_secret_detection_feature_card_spec.js
index bbd27cd4de4..7f2458aa539 100644
--- a/spec/frontend/security_configuration/components/pipeline_secret_detection_feature_card_spec.js
+++ b/spec/frontend/security_configuration/components/pipeline_secret_detection_feature_card_spec.js
@@ -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);
});
});
diff --git a/spec/frontend/work_items/graphql/cache_utils_spec.js b/spec/frontend/work_items/graphql/cache_utils_spec.js
index c23804e4c82..2e0d76531a9 100644
--- a/spec/frontend/work_items/graphql/cache_utils_spec.js
+++ b/spec/frontend/work_items/graphql/cache_utils_spec.js
@@ -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),
- }),
- }),
- }),
- }),
- );
- });
},
);
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 14783bcdd6b..9c9270fcc0b 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -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',
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 6759b512dfa..2d9dacdced2 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -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
diff --git a/spec/models/system_note_metadata_spec.rb b/spec/models/system_note_metadata_spec.rb
index c6b94a1c590..6a770b34d3e 100644
--- a/spec/models/system_note_metadata_spec.rb
+++ b/spec/models/system_note_metadata_spec.rb
@@ -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
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 029e2fbfefe..09a3b264cc9 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -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
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 235df5d9b8d..ed348ddb708 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -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
diff --git a/spec/services/work_items/parent_links/create_service_spec.rb b/spec/services/work_items/parent_links/create_service_spec.rb
index d6780c1f28f..2cd88bc3ee8 100644
--- a/spec/services/work_items/parent_links/create_service_spec.rb
+++ b/spec/services/work_items/parent_links/create_service_spec.rb
@@ -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
diff --git a/spec/services/work_items/parent_links/destroy_service_spec.rb b/spec/services/work_items/parent_links/destroy_service_spec.rb
index 3596e0a5934..02cbdbd47fb 100644
--- a/spec/services/work_items/parent_links/destroy_service_spec.rb
+++ b/spec/services/work_items/parent_links/destroy_service_spec.rb
@@ -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
diff --git a/spec/services/work_items/parent_links/reorder_service_spec.rb b/spec/services/work_items/parent_links/reorder_service_spec.rb
index 8afa295b05e..7619b896c8f 100644
--- a/spec/services/work_items/parent_links/reorder_service_spec.rb
+++ b/spec/services/work_items/parent_links/reorder_service_spec.rb
@@ -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
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index db2757072c7..746c391f7bd 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -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
diff --git a/spec/support/shared_examples/work_items/update_service_shared_examples.rb b/spec/support/shared_examples/work_items/update_service_shared_examples.rb
index 62dbce22975..2a8a80a2351 100644
--- a/spec/support/shared_examples/work_items/update_service_shared_examples.rb
+++ b/spec/support/shared_examples/work_items/update_service_shared_examples.rb
@@ -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)
diff --git a/yarn.lock b/yarn.lock
index 4161b776936..73f53455390 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"