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"