diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 8a99fe52329..52ffc3e24aa 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -123,17 +123,17 @@ config/bounded_contexts.yml @fabiopitino @grzesiek @stanhu @cwoolley-gitlab @tku /.gitlab/ci/test-on-gdk/ @gl-dx/pipeline-maintainers @gl-dx/maintainers /gems/gem.gitlab-ci.yml -[Tooling] @gl-dx/eng-prod +[Tooling] @gl-dx/tooling-maintainers Dangerfile /danger/ /tooling/ /spec/tooling/ /scripts/ -/scripts/**/*.rb @gl-dx/eng-prod @gitlab-org/maintainers/rails-backend -/scripts/**/*.js @gl-dx/eng-prod @gitlab-org/maintainers/frontend -/scripts/frontend/ @gl-dx/eng-prod @gitlab-org/maintainers/frontend -/scripts/remote_development/ @gl-dx/eng-prod @gitlab-org/maintainers/workspaces/backend @gitlab-org/maintainers/workspaces/frontend -/scripts/review_apps/seed-dast-test-data.sh @gl-dx/eng-prod @dappelt @ngeorge1 +/scripts/**/*.rb @gl-dx/tooling-maintainers @gitlab-org/maintainers/rails-backend +/scripts/**/*.js @gl-dx/tooling-maintainers @gitlab-org/maintainers/frontend +/scripts/frontend/ @gl-dx/tooling-maintainers @gitlab-org/maintainers/frontend +/scripts/remote_development/ @gl-dx/tooling-maintainers @gitlab-org/maintainers/workspaces/backend @gitlab-org/maintainers/workspaces/frontend +/scripts/review_apps/seed-dast-test-data.sh @gl-dx/tooling-maintainers @dappelt @ngeorge1 /.dockerignore /.editorconfig /.gitpod.yml @@ -145,7 +145,7 @@ Dangerfile /lefthook.yml /tests.yml -^[Backend Static Code Analysis] @gl-dx/eng-prod @dstull +^[Backend Static Code Analysis] @gl-dx/tooling-maintainers @dstull .rubocop*.yml /gems/config/rubocop.yml /rubocop/ @@ -990,7 +990,7 @@ lib/gitlab/checks/** /doc/development/fe_guide/keyboard_shortcuts.md @gitlab-org/foundations/personal-productivity/engineering /doc/development/git_object_deduplication.md @proglottis @toon /doc/development/gitaly.md @proglottis @toon -/doc/development/gitpod_internals.md @gl-dx/eng-prod +/doc/development/gitpod_internals.md @gl-dx/tooling-maintainers /doc/development/identity_verification.md @gitlab-org/software-supply-chain-security/authorization/approvers /doc/development/image_scaling.md @abdwdd @alexpooley /doc/development/internal_analytics/ @gitlab-org/analytics-section/product-analytics/engineers/frontend @gitlab-org/analytics-section/analytics-instrumentation/engineers @@ -1004,12 +1004,12 @@ lib/gitlab/checks/** /doc/development/organization/ @abdwdd @alexpooley /doc/development/permissions.md @gitlab-org/software-supply-chain-security/authorization/approvers /doc/development/permissions/ @gitlab-org/software-supply-chain-security/authorization/approvers -/doc/development/pipelines/ @gl-dx/eng-prod +/doc/development/pipelines/ @gl-dx/pipeline-maintainers /doc/development/policies.md @gitlab-org/software-supply-chain-security/authentication/approvers /doc/development/prometheus_metrics.md @gitlab-org/analytics-section/product-analytics/engineers/frontend /doc/development/search/ @gitlab-org/search-team/migration-maintainers /doc/development/sec/ @gitlab-org/secure/composition-analysis-be @gitlab-org/secure/static-analysis -/doc/development/software_design.md @gl-dx/eng-prod +/doc/development/software_design.md @gl-dx/pipeline-maintainers /doc/development/spam_protection_and_captcha/ @gitlab-org/software-supply-chain-security/authorization/approvers /doc/development/stage_group_observability/ @gitlab-org/analytics-section/product-analytics/engineers/frontend /doc/development/tracing.md @gitlab-org/analytics-section/product-analytics/engineers/frontend @@ -1252,7 +1252,7 @@ lib/gitlab/checks/** [Localization Team] @gitlab-com/localization/maintainers /doc-locale/** /doc/development/i18n/proofreader.md -/argo_translation.yml +/argo_translation.yml [Authorization] @gitlab-org/software-supply-chain-security/authorization/approvers /config/initializers/declarative_policy.rb diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml index 12ee41bcdc5..54dfe615e39 100644 --- a/.gitlab/ci/docs.gitlab-ci.yml +++ b/.gitlab/ci/docs.gitlab-ci.yml @@ -112,8 +112,22 @@ docs hugo_build: stage: lint needs: [] dependencies: [] + variables: + DOCS_BRANCH: "main" before_script: - - git clone --depth 1 --filter=tree:0 https://gitlab.com/gitlab-org/technical-writing/docs-gitlab-com.git + # Check if this a release branch, which would be the case for a backport. + # If this is a backport MR, we need to checkout the appropriate version + # of the Docs website. + - | + if [[ $CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ [0-9]+-[0-9]+-stable ]]; then + MAJOR=$(echo $CI_MERGE_REQUEST_TARGET_BRANCH_NAME | cut -d '-' -f 1) + MINOR=$(echo $CI_MERGE_REQUEST_TARGET_BRANCH_NAME | cut -d '-' -f 2) + # Convert GitLab style (17-9-stable-ee) to Docs style (17.9) + DOCS_BRANCH="$MAJOR.$MINOR" + echo "Using docs-gitlab-com branch $DOCS_BRANCH for release branch" + fi + # Clone the Docs website project + - git clone --depth 1 --filter=tree:0 --branch $DOCS_BRANCH https://gitlab.com/gitlab-org/technical-writing/docs-gitlab-com.git - cd docs-gitlab-com - make add-latest-icons # Copy the current project's docs to the appropriate location in the docs website diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index a2c932d1832..5123fbf0817 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -40,12 +40,8 @@ # - tooling/lib/tooling/glci/failure_analyzer.rb .failure_category_after_script: after_script: - # We need this at the very top, because the section_start/section_end logic is defined there. - source scripts/utils.sh - - | - section_start "failure-analyzer" "Report failure category" - tooling/lib/tooling/glci/failure_analyzer.rb $CI_JOB_ID || true - section_end "failure-analyzer" + - execute_failure_analyzer .repo-from-artifacts: variables: diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml index f17c5da7cec..0e003648217 100644 --- a/.gitlab/ci/qa-common/main.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml @@ -48,12 +48,8 @@ stages: # Taken from .gitlab/ci/global.gitlab-ci.yml, but adapted the paths .failure_category_after_script: after_script: - # We need this at the very top, because the section_start/section_end logic is defined there. - source $CI_PROJECT_DIR/scripts/utils.sh - - | - section_start "failure-analyzer" "Report failure category" - $CI_PROJECT_DIR/tooling/lib/tooling/glci/failure_analyzer.rb $CI_JOB_ID || true - section_end "failure-analyzer" + - execute_failure_analyzer .qa-install: extends: diff --git a/.gitlab/ci/release-environments/security.gitlab-ci.yml b/.gitlab/ci/release-environments/security.gitlab-ci.yml index 300bcfe88bd..b01da765fa5 100644 --- a/.gitlab/ci/release-environments/security.gitlab-ci.yml +++ b/.gitlab/ci/release-environments/security.gitlab-ci.yml @@ -6,7 +6,7 @@ include: inputs: cng_path: 'charts/components/images' - project: 'gitlab-org/quality/pipeline-common' - ref: '10.0.0' + ref: '10.1.0' file: ci/base.gitlab-ci.yml stages: diff --git a/.gitlab/ci/test-on-omnibus/external.gitlab-ci.yml b/.gitlab/ci/test-on-omnibus/external.gitlab-ci.yml index 15a9cb0e463..08917da062d 100644 --- a/.gitlab/ci/test-on-omnibus/external.gitlab-ci.yml +++ b/.gitlab/ci/test-on-omnibus/external.gitlab-ci.yml @@ -3,7 +3,7 @@ include: - project: gitlab-org/quality/pipeline-common - ref: 10.0.0 + ref: 10.1.0 file: - /ci/base.gitlab-ci.yml diff --git a/.rubocop_todo/gitlab/bounded_contexts.yml b/.rubocop_todo/gitlab/bounded_contexts.yml index f0f60280de9..de781647e00 100644 --- a/.rubocop_todo/gitlab/bounded_contexts.yml +++ b/.rubocop_todo/gitlab/bounded_contexts.yml @@ -1595,14 +1595,12 @@ Gitlab/BoundedContexts: - 'app/services/issues/after_create_service.rb' - 'app/services/issues/base_service.rb' - 'app/services/issues/build_service.rb' - - 'app/services/issues/clone_service.rb' - 'app/services/issues/close_service.rb' - 'app/services/issues/convert_to_ticket_service.rb' - 'app/services/issues/create_service.rb' - 'app/services/issues/duplicate_service.rb' - 'app/services/issues/export_csv_service.rb' - 'app/services/issues/import_csv_service.rb' - - 'app/services/issues/move_service.rb' - 'app/services/issues/prepare_import_csv_service.rb' - 'app/services/issues/referenced_merge_requests_service.rb' - 'app/services/issues/related_branches_service.rb' @@ -3132,12 +3130,10 @@ Gitlab/BoundedContexts: - 'ee/app/services/ee/issues/after_create_service.rb' - 'ee/app/services/ee/issues/base_service.rb' - 'ee/app/services/ee/issues/build_service.rb' - - 'ee/app/services/ee/issues/clone_service.rb' - 'ee/app/services/ee/issues/close_service.rb' - 'ee/app/services/ee/issues/create_service.rb' - 'ee/app/services/ee/issues/export_csv_service.rb' - 'ee/app/services/ee/issues/import_csv_service.rb' - - 'ee/app/services/ee/issues/move_service.rb' - 'ee/app/services/ee/issues/reopen_service.rb' - 'ee/app/services/ee/issues/update_service.rb' - 'ee/app/services/ee/jira_connect/sync_service.rb' diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml index 94544381abe..6bb4a643980 100644 --- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml +++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml @@ -540,7 +540,6 @@ Layout/EmptyLineAfterMagicComment: - 'spec/services/dependency_proxy/head_manifest_service_spec.rb' - 'spec/services/dependency_proxy/request_token_service_spec.rb' - 'spec/services/design_management/copy_design_collection/copy_service_spec.rb' - - 'spec/services/design_management/copy_design_collection/queue_service_spec.rb' - 'spec/services/design_management/delete_designs_service_spec.rb' - 'spec/services/design_management/move_designs_service_spec.rb' - 'spec/services/design_management/save_designs_service_spec.rb' diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index 0fbd0bd3eb3..c9de674bb40 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -236,7 +236,6 @@ Layout/LineLength: - 'app/services/dependency_proxy/group_settings/update_service.rb' - 'app/services/dependency_proxy/image_ttl_group_policies/update_service.rb' - 'app/services/design_management/copy_design_collection/copy_service.rb' - - 'app/services/design_management/copy_design_collection/queue_service.rb' - 'app/services/design_management/generate_image_versions_service.rb' - 'app/services/design_management/save_designs_service.rb' - 'app/services/discussions/resolve_service.rb' @@ -253,10 +252,8 @@ Layout/LineLength: - 'app/services/import/bitbucket_server_service.rb' - 'app/services/issuable/process_assignees.rb' - 'app/services/issuable_base_service.rb' - - 'app/services/issues/clone_service.rb' - 'app/services/issues/close_service.rb' - 'app/services/issues/duplicate_service.rb' - - 'app/services/issues/move_service.rb' - 'app/services/issues/relative_position_rebalancing_service.rb' - 'app/services/issues/set_crm_contacts_service.rb' - 'app/services/issues/update_service.rb' @@ -668,7 +665,6 @@ Layout/LineLength: - 'ee/app/services/ee/groups/update_service.rb' - 'ee/app/services/ee/ip_restrictions/update_service.rb' - 'ee/app/services/ee/issues/base_service.rb' - - 'ee/app/services/ee/issues/clone_service.rb' - 'ee/app/services/ee/merge_requests/merge_base_service.rb' - 'ee/app/services/ee/merge_requests/refresh_service.rb' - 'ee/app/services/ee/personal_access_tokens/revoke_service.rb' @@ -1647,7 +1643,6 @@ Layout/LineLength: - 'ee/spec/services/ee/ip_restrictions/update_service_spec.rb' - 'ee/spec/services/ee/issuable/common_system_notes_service_spec.rb' - 'ee/spec/services/ee/issue_links/create_service_spec.rb' - - 'ee/spec/services/ee/issues/clone_service_spec.rb' - 'ee/spec/services/ee/issues/create_service_spec.rb' - 'ee/spec/services/ee/issues/update_service_spec.rb' - 'ee/spec/services/ee/members/destroy_service_spec.rb' @@ -3796,12 +3791,10 @@ Layout/LineLength: - 'spec/services/issue_links/create_service_spec.rb' - 'spec/services/issues/after_create_service_spec.rb' - 'spec/services/issues/build_service_spec.rb' - - 'spec/services/issues/clone_service_spec.rb' - 'spec/services/issues/close_service_spec.rb' - 'spec/services/issues/create_service_spec.rb' - 'spec/services/issues/duplicate_service_spec.rb' - 'spec/services/issues/export_csv_service_spec.rb' - - 'spec/services/issues/move_service_spec.rb' - 'spec/services/issues/referenced_merge_requests_service_spec.rb' - 'spec/services/issues/relative_position_rebalancing_service_spec.rb' - 'spec/services/issues/resolve_discussions_spec.rb' diff --git a/.rubocop_todo/lint/assignment_in_condition.yml b/.rubocop_todo/lint/assignment_in_condition.yml index b94b7766482..28bba5d1955 100644 --- a/.rubocop_todo/lint/assignment_in_condition.yml +++ b/.rubocop_todo/lint/assignment_in_condition.yml @@ -102,8 +102,6 @@ Lint/AssignmentInCondition: - 'ee/app/services/deployments/approval_service.rb' - 'ee/app/services/dora/aggregate_metrics_service.rb' - 'ee/app/services/ee/application_settings/update_service.rb' - - 'ee/app/services/ee/issues/clone_service.rb' - - 'ee/app/services/ee/issues/move_service.rb' - 'ee/app/services/ee/projects/operations/update_service.rb' - 'ee/app/services/gitlab_subscriptions/fetch_subscription_plans_service.rb' - 'ee/app/services/incident_management/issuable_resource_links/zoom_link_service.rb' diff --git a/.rubocop_todo/lint/unused_method_argument.yml b/.rubocop_todo/lint/unused_method_argument.yml index 6bd9efc80db..46aea816e7b 100644 --- a/.rubocop_todo/lint/unused_method_argument.yml +++ b/.rubocop_todo/lint/unused_method_argument.yml @@ -140,7 +140,6 @@ Lint/UnusedMethodArgument: - 'app/services/groups/update_service.rb' - 'app/services/import/gitlab_projects/file_acquisition_strategies/file_upload.rb' - 'app/services/issues/export_csv_service.rb' - - 'app/services/issues/move_service.rb' - 'app/services/jira/requests/base.rb' - 'app/services/keys/destroy_service.rb' - 'app/services/merge_requests/close_service.rb' diff --git a/.rubocop_todo/rails/date.yml b/.rubocop_todo/rails/date.yml index 1c76fb5df6d..ffe598fd901 100644 --- a/.rubocop_todo/rails/date.yml +++ b/.rubocop_todo/rails/date.yml @@ -202,7 +202,6 @@ Rails/Date: - 'spec/services/import/reassign_placeholder_user_records_service_spec.rb' - 'spec/services/issuable/callbacks/time_tracking_spec.rb' - 'spec/services/issuable/common_system_notes_service_spec.rb' - - 'spec/services/issues/move_service_spec.rb' - 'spec/services/members/invitation_reminder_email_service_spec.rb' - 'spec/services/members/update_service_spec.rb' - 'spec/services/milestones/find_or_create_service_spec.rb' diff --git a/.rubocop_todo/rspec/be_eq.yml b/.rubocop_todo/rspec/be_eq.yml index a7b8d9787ec..ba0dbe981e2 100644 --- a/.rubocop_todo/rspec/be_eq.yml +++ b/.rubocop_todo/rspec/be_eq.yml @@ -1310,10 +1310,8 @@ RSpec/BeEq: - 'spec/services/incident_management/issuable_escalation_statuses/create_service_spec.rb' - 'spec/services/incident_management/timeline_events/create_service_spec.rb' - 'spec/services/issuable/callbacks/time_tracking_spec.rb' - - 'spec/services/issues/clone_service_spec.rb' - 'spec/services/issues/create_service_spec.rb' - 'spec/services/issues/export_csv_service_spec.rb' - - 'spec/services/issues/move_service_spec.rb' - 'spec/services/issues/update_service_spec.rb' - 'spec/services/issues/zoom_link_service_spec.rb' - 'spec/services/jira_connect_installations/update_service_spec.rb' diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml index 1c7a1728da9..0c84b90e627 100644 --- a/.rubocop_todo/rspec/before_all_role_assignment.yml +++ b/.rubocop_todo/rspec/before_all_role_assignment.yml @@ -534,7 +534,6 @@ RSpec/BeforeAllRoleAssignment: - 'ee/spec/services/ee/groups/deploy_tokens/revoke_service_spec.rb' - 'ee/spec/services/ee/groups/group_links/create_service_spec.rb' - 'ee/spec/services/ee/issuable/bulk_update_service_spec.rb' - - 'ee/spec/services/ee/issues/clone_service_spec.rb' - 'ee/spec/services/ee/issues/close_service_spec.rb' - 'ee/spec/services/ee/issues/create_service_spec.rb' - 'ee/spec/services/ee/issues/reopen_service_spec.rb' @@ -1210,9 +1209,7 @@ RSpec/BeforeAllRoleAssignment: - 'spec/services/integrations/slack_options/user_search_handler_spec.rb' - 'spec/services/issuable/bulk_update_service_spec.rb' - 'spec/services/issue_links/create_service_spec.rb' - - 'spec/services/issues/clone_service_spec.rb' - 'spec/services/issues/create_service_spec.rb' - - 'spec/services/issues/move_service_spec.rb' - 'spec/services/issues/reorder_service_spec.rb' - 'spec/services/issues/set_crm_contacts_service_spec.rb' - 'spec/services/issues/update_service_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index 0e61e80c68a..1ac7155155c 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -616,9 +616,7 @@ RSpec/ContextWording: - 'ee/spec/services/ee/integrations/test/project_service_spec.rb' - 'ee/spec/services/ee/ip_restrictions/update_service_spec.rb' - 'ee/spec/services/ee/issuable/bulk_update_service_spec.rb' - - 'ee/spec/services/ee/issues/clone_service_spec.rb' - 'ee/spec/services/ee/issues/create_service_spec.rb' - - 'ee/spec/services/ee/issues/move_service_spec.rb' - 'ee/spec/services/ee/issues/update_service_spec.rb' - 'ee/spec/services/ee/keys/destroy_service_spec.rb' - 'ee/spec/services/ee/members/create_service_spec.rb' @@ -2387,11 +2385,9 @@ RSpec/ContextWording: - 'spec/services/integrations/test/project_service_spec.rb' - 'spec/services/issue_links/list_service_spec.rb' - 'spec/services/issues/build_service_spec.rb' - - 'spec/services/issues/clone_service_spec.rb' - 'spec/services/issues/close_service_spec.rb' - 'spec/services/issues/create_service_spec.rb' - 'spec/services/issues/export_csv_service_spec.rb' - - 'spec/services/issues/move_service_spec.rb' - 'spec/services/issues/referenced_merge_requests_service_spec.rb' - 'spec/services/issues/related_branches_service_spec.rb' - 'spec/services/issues/relative_position_rebalancing_service_spec.rb' diff --git a/.rubocop_todo/rspec/expect_change.yml b/.rubocop_todo/rspec/expect_change.yml index 676670d40f9..a206eeb3b04 100644 --- a/.rubocop_todo/rspec/expect_change.yml +++ b/.rubocop_todo/rspec/expect_change.yml @@ -270,7 +270,6 @@ RSpec/ExpectChange: - 'spec/services/clusters/agents/create_activity_event_service_spec.rb' - 'spec/services/clusters/agents/delete_expired_events_service_spec.rb' - 'spec/services/clusters/kubernetes/create_or_update_namespace_service_spec.rb' - - 'spec/services/design_management/copy_design_collection/queue_service_spec.rb' - 'spec/services/design_management/save_designs_service_spec.rb' - 'spec/services/event_create_service_spec.rb' - 'spec/services/events/destroy_service_spec.rb' @@ -285,7 +284,6 @@ RSpec/ExpectChange: - 'spec/services/issuable/bulk_update_service_spec.rb' - 'spec/services/issues/create_service_spec.rb' - 'spec/services/issues/export_csv_service_spec.rb' - - 'spec/services/issues/move_service_spec.rb' - 'spec/services/issues/update_service_spec.rb' - 'spec/services/jira_connect_installations/destroy_service_spec.rb' - 'spec/services/keys/destroy_service_spec.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index 0b24bb99674..cabb9f6ee8d 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -854,7 +854,6 @@ RSpec/NamedSubject: - 'ee/spec/services/ee/issuable/common_system_notes_service_spec.rb' - 'ee/spec/services/ee/issuable/destroy_service_spec.rb' - 'ee/spec/services/ee/issue_links/create_service_spec.rb' - - 'ee/spec/services/ee/issues/clone_service_spec.rb' - 'ee/spec/services/ee/issues/update_service_spec.rb' - 'ee/spec/services/ee/keys/destroy_service_spec.rb' - 'ee/spec/services/ee/members/create_service_spec.rb' @@ -2884,7 +2883,6 @@ RSpec/NamedSubject: - 'spec/services/deployments/archive_in_project_service_spec.rb' - 'spec/services/deployments/update_environment_service_spec.rb' - 'spec/services/design_management/copy_design_collection/copy_service_spec.rb' - - 'spec/services/design_management/copy_design_collection/queue_service_spec.rb' - 'spec/services/design_management/design_user_notes_count_service_spec.rb' - 'spec/services/design_management/move_designs_service_spec.rb' - 'spec/services/discussions/capture_diff_note_position_service_spec.rb' @@ -2941,12 +2939,10 @@ RSpec/NamedSubject: - 'spec/services/issuable/import_csv/base_service_spec.rb' - 'spec/services/issue_links/list_service_spec.rb' - 'spec/services/issues/build_service_spec.rb' - - 'spec/services/issues/clone_service_spec.rb' - 'spec/services/issues/create_service_spec.rb' - 'spec/services/issues/duplicate_service_spec.rb' - 'spec/services/issues/export_csv_service_spec.rb' - 'spec/services/issues/import_csv_service_spec.rb' - - 'spec/services/issues/move_service_spec.rb' - 'spec/services/issues/prepare_import_csv_service_spec.rb' - 'spec/services/issues/relative_position_rebalancing_service_spec.rb' - 'spec/services/issues/reorder_service_spec.rb' diff --git a/.rubocop_todo/rspec/receive_messages.yml b/.rubocop_todo/rspec/receive_messages.yml index 6633bda74b3..36629016f45 100644 --- a/.rubocop_todo/rspec/receive_messages.yml +++ b/.rubocop_todo/rspec/receive_messages.yml @@ -136,7 +136,6 @@ RSpec/ReceiveMessages: - 'ee/spec/services/compliance_management/frameworks/update_project_service_spec.rb' - 'ee/spec/services/ee/ci/job_token_scope/remove_group_service_spec.rb' - 'ee/spec/services/ee/ci/job_token_scope/remove_project_service_spec.rb' - - 'ee/spec/services/ee/issues/move_service_spec.rb' - 'ee/spec/services/ee/post_receive_service_spec.rb' - 'ee/spec/services/ee/spam/spam_verdict_service_spec.rb' - 'ee/spec/services/ee/work_items/related_work_item_links/create_service_spec.rb' diff --git a/.rubocop_todo/rspec/subject_declaration.yml b/.rubocop_todo/rspec/subject_declaration.yml index 052605f94d5..0453097f6a1 100644 --- a/.rubocop_todo/rspec/subject_declaration.yml +++ b/.rubocop_todo/rspec/subject_declaration.yml @@ -85,8 +85,6 @@ RSpec/SubjectDeclaration: - 'spec/serializers/pipeline_details_entity_spec.rb' - 'spec/services/concerns/exclusive_lease_guard_spec.rb' - 'spec/services/concerns/rate_limited_service_spec.rb' - - 'spec/services/issues/clone_service_spec.rb' - - 'spec/services/issues/move_service_spec.rb' - 'spec/services/issues/prepare_import_csv_service_spec.rb' - 'spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb' - 'spec/services/merge_requests/reload_diffs_service_spec.rb' diff --git a/.rubocop_todo/style/accessor_grouping.yml b/.rubocop_todo/style/accessor_grouping.yml index 55d6f2737a3..bba30802d1b 100644 --- a/.rubocop_todo/style/accessor_grouping.yml +++ b/.rubocop_todo/style/accessor_grouping.yml @@ -19,7 +19,6 @@ Style/AccessorGrouping: - 'app/models/integrations/chat_message/wiki_page_message.rb' - 'app/models/project.rb' - 'app/services/deployments/update_environment_service.rb' - - 'app/services/issues/clone_service.rb' - 'app/services/note_summary.rb' - 'app/services/notification_recipients/builder/default.rb' - 'app/services/task_list_toggle_service.rb' diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml index 2d38d94298d..41a99a3c92e 100644 --- a/.rubocop_todo/style/format_string.yml +++ b/.rubocop_todo/style/format_string.yml @@ -58,9 +58,7 @@ Style/FormatString: - 'app/services/import/bitbucket_server_service.rb' - 'app/services/import/fogbugz_service.rb' - 'app/services/issuable_links/create_service.rb' - - 'app/services/issues/clone_service.rb' - 'app/services/issues/close_service.rb' - - 'app/services/issues/move_service.rb' - 'app/services/issues/set_crm_contacts_service.rb' - 'app/services/jira/requests/base.rb' - 'app/services/milestones/promote_service.rb' diff --git a/.rubocop_todo/style/guard_clause.yml b/.rubocop_todo/style/guard_clause.yml index ae60e377bf9..4cf896c5a00 100644 --- a/.rubocop_todo/style/guard_clause.yml +++ b/.rubocop_todo/style/guard_clause.yml @@ -130,8 +130,6 @@ Style/GuardClause: - 'app/services/issuable/bulk_update_service.rb' - 'app/services/issuable/common_system_notes_service.rb' - 'app/services/issuable_base_service.rb' - - 'app/services/issues/clone_service.rb' - - 'app/services/issues/move_service.rb' - 'app/services/issues/update_service.rb' - 'app/services/markdown_content_rewriter_service.rb' - 'app/services/merge_requests/add_spent_time_service.rb' @@ -263,7 +261,6 @@ Style/GuardClause: - 'ee/app/services/ee/groups/update_service.rb' - 'ee/app/services/ee/issuable/common_system_notes_service.rb' - 'ee/app/services/ee/issues/base_service.rb' - - 'ee/app/services/ee/issues/clone_service.rb' - 'ee/app/services/ee/merge_requests/merge_base_service.rb' - 'ee/app/services/ee/merge_requests/refresh_service.rb' - 'ee/app/services/ee/projects/create_service.rb' diff --git a/.rubocop_todo/style/if_unless_modifier.yml b/.rubocop_todo/style/if_unless_modifier.yml index 6ccb6357d17..b7f9fdaf2ee 100644 --- a/.rubocop_todo/style/if_unless_modifier.yml +++ b/.rubocop_todo/style/if_unless_modifier.yml @@ -93,7 +93,6 @@ Style/IfUnlessModifier: - 'app/services/concerns/merge_requests/assigns_merge_params.rb' - 'app/services/dependency_proxy/group_settings/update_service.rb' - 'app/services/dependency_proxy/image_ttl_group_policies/update_service.rb' - - 'app/services/design_management/copy_design_collection/queue_service.rb' - 'app/services/discussions/resolve_service.rb' - 'app/services/discussions/update_diff_position_service.rb' - 'app/services/draft_notes/create_service.rb' @@ -108,7 +107,6 @@ Style/IfUnlessModifier: - 'app/services/issuable/bulk_update_service.rb' - 'app/services/issuable_base_service.rb' - 'app/services/issuable_links/create_service.rb' - - 'app/services/issues/move_service.rb' - 'app/services/issues/relative_position_rebalancing_service.rb' - 'app/services/issues/update_service.rb' - 'app/services/lfs/lock_file_service.rb' diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index 536664d6369..675162ebbea 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -db0830c3c2751e55a8a4702cf08ff756f09e7dc8 +1be1a77766ed02eefd5fc8e64e99e7b69e5ed91b diff --git a/app/assets/javascripts/entrypoints/coverage_persistence.js b/app/assets/javascripts/entrypoints/coverage_persistence.js new file mode 100644 index 00000000000..c9eeccf0afb --- /dev/null +++ b/app/assets/javascripts/entrypoints/coverage_persistence.js @@ -0,0 +1,47 @@ +/* eslint-disable no-underscore-dangle,@gitlab/require-i18n-strings,no-console */ + +function getPersistedCoverage() { + const storedPaths = localStorage.getItem('__coverage_paths__'); + if (storedPaths) { + return JSON.parse(storedPaths); + } + return []; +} + +function getCoverage() { + if (!window.__coverage__) { + console.warn( + 'Coverage object is missing on the page. Did you install Istanbul babel plugin and enable Webpack?', + ); + } + const filePaths = Object.keys(window.__coverage__); + const existingPaths = getPersistedCoverage(); + return [...new Set([...existingPaths, ...filePaths])]; +} + +function persistCoverage(coverage = getCoverage()) { + localStorage.setItem('__coverage_paths__', JSON.stringify(coverage)); + console.log(`Coverage paths saved: ${coverage.length} files tracked`); +} + +function updateCoverage() { + const coverage = getCoverage(); + persistCoverage(coverage); + window.__coverageFilePaths = coverage; +} + +window.addEventListener('beforeunload', () => { + updateCoverage(); +}); + +window.__coveragePathsPersistence = { + update: updateCoverage, + getPaths() { + return window.__coverageFilePaths || []; + }, + reset() { + localStorage.removeItem('__coverage_paths__'); + window.__coverageFilePaths = []; + console.log('Coverage paths reset.'); + }, +}; diff --git a/app/assets/javascripts/lib/utils/secret_detection.js b/app/assets/javascripts/lib/utils/secret_detection.js index f226392bf16..b31601de37c 100644 --- a/app/assets/javascripts/lib/utils/secret_detection.js +++ b/app/assets/javascripts/lib/utils/secret_detection.js @@ -35,22 +35,11 @@ const i18n = { helpString: __('Why am I seeing this warning?'), }; -const redactString = (inputString) => { - if (inputString.length <= 9) return inputString; - - const prefix = inputString.substring(0, 5); // Keep the first 5 characters - const suffix = inputString.substring(inputString.length - 4); // Keep the last 4 characters - const redactLength = Math.min(inputString.length - prefix.length - suffix.length, 22); - - return `${prefix}${'*'.repeat(redactLength)}${suffix}`; -}; - const formatMessage = (findings, contentType) => { const header = sprintf(i18n.promptMessage(findings.length), { contentType }); const matchedPatterns = findings.map(({ patternName, matchedString }) => { - const redactedString = redactString(matchedString); - return `
  • ${escape(patternName)}: ${escape(redactedString)}
  • `; + return `
  • ${escape(patternName)}: ${escape(matchedString)}
  • `; }); const message = ` @@ -74,10 +63,10 @@ const containsSensitiveToken = (message) => { for (const rule of sensitiveDataPatterns()) { const regex = new RegExp(rule.regex, 'gi'); - const matches = message.match(regex); + const uniqueMatches = new Set(message.match(regex)); - if (matches) { - matches.forEach((match) => { + if (uniqueMatches) { + uniqueMatches.forEach((match) => { findings.push({ patternName: rule.name, matchedString: match, diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss index 8d6783f3bb6..e243929f255 100644 --- a/app/assets/stylesheets/framework/blocks.scss +++ b/app/assets/stylesheets/framework/blocks.scss @@ -16,10 +16,6 @@ } } -.oneline { - line-height: 35px; -} - .row-content-block { margin-top: 0; @apply gl-bg-subtle; diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 66c2149d447..cab3ed92302 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -199,13 +199,9 @@ class Projects::IssuesController < Projects::ApplicationController new_project = Project.find(params[:move_to_project_id]) return render_404 unless issue.can_move?(current_user, new_project) - @issue = if project.work_item_move_and_clone_flag_enabled? - ::WorkItems::DataSync::MoveService.new( - work_item: issue, current_user: current_user, target_namespace: new_project.project_namespace - ).execute[:work_item] - else - ::Issues::MoveService.new(container: project, current_user: current_user).execute(issue, new_project) - end + @issue = ::WorkItems::DataSync::MoveService.new( + work_item: issue, current_user: current_user, target_namespace: new_project.project_namespace + ).execute[:work_item] end respond_to do |format| diff --git a/app/graphql/mutations/issues/move.rb b/app/graphql/mutations/issues/move.rb index 667dabad8f9..64956ac74e9 100644 --- a/app/graphql/mutations/issues/move.rb +++ b/app/graphql/mutations/issues/move.rb @@ -14,25 +14,16 @@ module Mutations Gitlab::QueryLimiting.disable!('https://gitlab.com/gitlab-org/gitlab/-/issues/20816') issue = authorized_find!(project_path: project_path, iid: iid) - source_project = issue.project target_project = resolve_project(full_path: target_project_path).sync begin - moved_issue = if source_project.work_item_move_and_clone_flag_enabled? - response = ::WorkItems::DataSync::MoveService.new( - work_item: issue, current_user: current_user, - target_namespace: target_project.project_namespace - ).execute + response = ::WorkItems::DataSync::MoveService.new( + work_item: issue, current_user: current_user, + target_namespace: target_project.project_namespace + ).execute - errors = response.message if response.error? - response.payload[:work_item] - else - ::Issues::MoveService.new( - container: source_project, current_user: current_user - ).execute(issue, target_project) - end - rescue ::Issues::MoveService::MoveError => e - errors = e.message + errors = response.message if response.error? + moved_issue = response.payload[:work_item] end { diff --git a/app/graphql/resolvers/work_items/linked_items_resolver.rb b/app/graphql/resolvers/work_items/linked_items_resolver.rb index 0e01b17adb8..e27082555de 100644 --- a/app/graphql/resolvers/work_items/linked_items_resolver.rb +++ b/app/graphql/resolvers/work_items/linked_items_resolver.rb @@ -14,17 +14,17 @@ module Resolvers type ::Types::WorkItems::LinkedItemType.connection_type, null: true def resolve_with_lookahead(**args) - apply_lookahead(related_work_items(args)) + if Feature.enabled?(:batch_load_linked_items, work_item.resource_parent, type: :wip) + bulk_load_linked_items(args[:filter]) + else + offset_pagination( + work_item.linked_work_items(authorize: false, link_type: args[:filter]) + ) + end end private - def related_work_items(args) - offset_pagination( - work_item.linked_work_items(authorize: false, link_type: args[:filter]) - ) - end - def work_item object.is_a?(Issue) ? WorkItem.find_by_id(object.id) : object.work_item end @@ -33,6 +33,34 @@ module Resolvers def node_selection(selection = lookahead) super.selection(:work_item) end + + def bulk_load_linked_items(link_type) + # Calculate the current nesting level of linked items in the context path + nesting_level = context[:current_path].count('linkedItems') + batch_key = "linked_items_level_#{nesting_level}" + + BatchLoader::GraphQL.for(work_item.id).batch(key: batch_key, cache: false) do |item_ids, loader, _args| + preloads = [:author, :work_item_type, { project: [:route, { namespace: :route }] }] + linked_items = apply_lookahead(WorkItem.linked_items_for(item_ids, preload: preloads, link_type: link_type)) + grouped_by_source = linked_items_grouped_by_source(linked_items, item_ids) + + # Assign the grouped items to each work item ID in the batch loader + item_ids.each do |id| + loader.call(id, grouped_by_source[id] || []) + end + end + end + + def linked_items_grouped_by_source(linked_items, item_ids) + linked_items.each_with_object({}) do |item, result| + # Find the ID of the item that this item links to + target_id = [item.issue_link_source_id, item.issue_link_target_id].find { |id| id != item.id } + next unless item_ids.include?(target_id) + + result[target_id] ||= [] + result[target_id] << item + end + end end end end diff --git a/app/graphql/types/ci/runner_job_execution_status_enum.rb b/app/graphql/types/ci/runner_job_execution_status_enum.rb index 421e1758536..986783bc9ab 100644 --- a/app/graphql/types/ci/runner_job_execution_status_enum.rb +++ b/app/graphql/types/ci/runner_job_execution_status_enum.rb @@ -7,13 +7,10 @@ module Types value 'IDLE', description: "Runner is idle.", - value: :idle, - experiment: { milestone: '15.7' } - + value: :idle value 'ACTIVE', description: 'Runner is busy.', - value: :active, - experiment: { milestone: '17.2' } + value: :active end end end diff --git a/app/graphql/types/ci/runner_manager_type.rb b/app/graphql/types/ci/runner_manager_type.rb index c1b374756b0..de593719b69 100644 --- a/app/graphql/types/ci/runner_manager_type.rb +++ b/app/graphql/types/ci/runner_manager_type.rb @@ -27,8 +27,7 @@ module Types field :job_execution_status, Types::Ci::RunnerJobExecutionStatusEnum, null: true, - description: 'Job execution status of the runner manager.', - experiment: { milestone: '16.3' } + description: 'Job execution status of the runner manager.' field :platform_name, GraphQL::Types::String, null: true, description: 'Platform provided by the runner manager.', method: :platform diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index c400f41d91c..f0185072948 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -61,8 +61,7 @@ module Types field :job_execution_status, Types::Ci::RunnerJobExecutionStatusEnum, null: true, - description: 'Job execution status of the runner.', - experiment: { milestone: '15.7' } + description: 'Job execution status of the runner.' field :jobs, ::Types::Ci::JobInterface.connection_type, null: true, description: 'Jobs assigned to the runner. This field can only be resolved for one runner in any single request.', authorize: :read_builds, diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index 4831e9aa04b..f0843e21b5a 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -41,10 +41,6 @@ module TreeHelper # `username-branchname-patch-epoch` # where `epoch` is the last 5 digits of the time since epoch (in # milliseconds) - # - # Note: this correlates with how the WebIDE formats the branch name - # and if this implementation changes, so should the `placeholderBranchName` - # definition in app/assets/javascripts/ide/stores/modules/commit/getters.js def patch_branch_name(ref) return unless current_user diff --git a/app/models/group.rb b/app/models/group.rb index 166f5ec5789..52ee3940305 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1048,10 +1048,6 @@ class Group < Namespace ].compact.min end - def work_item_move_and_clone_flag_enabled? - feature_flag_enabled_for_self_or_ancestor?(:work_item_move_and_clone, type: :beta) - end - def work_items_feature_flag_enabled? feature_flag_enabled_for_self_or_ancestor?(:work_items) end diff --git a/app/models/namespaces/project_namespace.rb b/app/models/namespaces/project_namespace.rb index 73e5a79647e..aa0dc904870 100644 --- a/app/models/namespaces/project_namespace.rb +++ b/app/models/namespaces/project_namespace.rb @@ -71,10 +71,6 @@ module Namespaces assign_attributes(attributes_to_sync) end - def work_item_move_and_clone_flag_enabled? - project.work_item_move_and_clone_flag_enabled? - end - # It's always 1 project but it has to be an AR relation def all_projects Project.where(id: project.id) diff --git a/app/models/project.rb b/app/models/project.rb index a6c24fc92af..90d9b28efc4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -3367,10 +3367,6 @@ class Project < ApplicationRecord pending_delete? || hidden? end - def work_item_move_and_clone_flag_enabled? - Feature.enabled?(:work_item_move_and_clone, self, type: :beta) || group&.work_item_move_and_clone_flag_enabled? - end - def work_items_feature_flag_enabled? group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self) end diff --git a/app/models/work_item.rb b/app/models/work_item.rb index a872e75aa74..07c42498478 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -130,6 +130,19 @@ class WorkItem < Issue ]) end + def linked_items_for(target_ids, preload: nil, link_type: nil) + select_query = + select('issues.*, + issue_links.id AS issue_link_id, + issue_links.link_type AS issue_link_type_value, + issue_links.target_id AS issue_link_source_id, + issue_links.source_id AS issue_link_target_id, + issue_links.created_at AS issue_link_created_at, + issue_links.updated_at AS issue_link_updated_at') + + ordered_linked_items(select_query, ids: target_ids, link_type: link_type, preload: preload) + end + override :related_link_class def related_link_class WorkItems::RelatedWorkItemLink @@ -144,6 +157,25 @@ class WorkItem < Issue def non_widgets [:pending_escalations] end + + def ordered_linked_items(select_query, ids: [], link_type: nil, preload: nil) + type_condition = + if link_type == WorkItems::RelatedWorkItemLink::TYPE_RELATES_TO + " AND issue_links.link_type = #{WorkItems::RelatedWorkItemLink.link_types[link_type]}" + else + "" + end + + query_ids = sanitize_sql_array(['?', Array.wrap(ids)]) + + select_query + .joins("INNER JOIN issue_links ON + (issue_links.source_id = issues.id AND issue_links.target_id IN (#{query_ids})#{type_condition}) + OR + (issue_links.target_id = issues.id AND issue_links.source_id IN (#{query_ids})#{type_condition})") + .preload(preload) + .reorder(linked_items_keyset_order) + end end def create_dates_source_from_current_dates @@ -254,14 +286,14 @@ class WorkItem < Issue def linked_work_items(current_user = nil, authorize: true, preload: nil, link_type: nil) return [] if new_record? - linked_work_items = linked_work_items_query(link_type) - .preload(preload) - .reorder(self.class.linked_items_keyset_order) - return linked_work_items unless authorize + linked_items = + self.class.ordered_linked_items(linked_issues_select, ids: id, link_type: link_type, preload: preload) + + return linked_items unless authorize cross_project_filter = ->(work_items) { work_items.where(project: project) } Ability.work_items_readable_by_user( - linked_work_items, + linked_items, current_user, filters: { read_cross_project: cross_project_filter } ) @@ -400,21 +432,6 @@ class WorkItem < Issue end end - def linked_work_items_query(link_type) - type_condition = - if link_type == WorkItems::RelatedWorkItemLink::TYPE_RELATES_TO - " AND issue_links.link_type = #{WorkItems::RelatedWorkItemLink.link_types[link_type]}" - else - "" - end - - linked_issues_select - .joins("INNER JOIN issue_links ON - (issue_links.source_id = issues.id AND issue_links.target_id = #{id}#{type_condition}) - OR - (issue_links.target_id = issues.id AND issue_links.source_id = #{id}#{type_condition})") - end - def hierarchy_supports_parent? ::WorkItems::HierarchyRestriction.find_by_child_type_id(work_item_type_id).present? end diff --git a/app/services/design_management/copy_design_collection/queue_service.rb b/app/services/design_management/copy_design_collection/queue_service.rb deleted file mode 100644 index f76917dbe47..00000000000 --- a/app/services/design_management/copy_design_collection/queue_service.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -# Service for setting the initial copy_state on the target DesignCollection -# and queuing a CopyDesignCollectionWorker. -module DesignManagement - module CopyDesignCollection - class QueueService - def initialize(current_user, issue, target_issue) - @current_user = current_user - @issue = issue - @target_issue = target_issue - @target_design_collection = target_issue.design_collection - end - - def execute - return error('User cannot copy designs to issue') unless user_can_copy? - return error('Target design collection copy state must be `ready`') unless target_design_collection.can_start_copy? - - target_design_collection.start_copy! - - DesignManagement::CopyDesignCollectionWorker.perform_async(current_user.id, issue.id, target_issue.id) - - ServiceResponse.success - end - - private - - delegate :design_collection, to: :issue - - attr_reader :current_user, :issue, :target_design_collection, :target_issue - - def error(message) - ServiceResponse.error(message: message) - end - - def user_can_copy? - current_user.can?(:read_design, issue) && - current_user.can?(:admin_issue, target_issue) - end - end - end -end diff --git a/app/services/issues/clone_service.rb b/app/services/issues/clone_service.rb deleted file mode 100644 index faec86c6c18..00000000000 --- a/app/services/issues/clone_service.rb +++ /dev/null @@ -1,124 +0,0 @@ -# frozen_string_literal: true - -module Issues - class CloneService < Issuable::Clone::BaseService - CloneError = Class.new(StandardError) - - def execute(issue, target_project, with_notes: false) - @target_project = target_project - @with_notes = with_notes - - verify_can_clone_issue!(issue, target_project) - - super(issue, target_project) - - notify_participants - - queue_copy_designs - - new_entity - end - - private - - attr_reader :target_project - attr_reader :with_notes - - def verify_can_clone_issue!(issue, target_project) - unless issue.supports_move_and_clone? - raise CloneError, s_('CloneIssue|Cannot clone issues of \'%{issue_type}\' type.') % { issue_type: issue.issue_type } - end - - unless issue.can_clone?(current_user, target_project) - raise CloneError, s_('CloneIssue|Cannot clone issue due to insufficient permissions.') - end - - if target_project.pending_delete? - raise CloneError, s_('CloneIssue|Cannot clone issue to target project as it is pending deletion.') - end - end - - def update_new_entity - # we don't call `super` because we want to be able to decide whether or not to copy all comments over. - update_new_entity_description - - if with_notes - copy_notes - copy_resource_events - end - end - - def update_old_entity - # no-op - # The base_service closes the old issue, we don't want that, so we override here so nothing happens. - end - - def create_new_entity - new_params = { - id: nil, - iid: nil, - relative_position: relative_position, - project: target_project, - author: current_user, - assignee_ids: original_entity.assignee_ids - } - - new_params = original_entity.serializable_hash.symbolize_keys.except(:project_id, :author_id).merge(new_params) - new_params = new_params.merge(rewritten_old_entity_attributes) - new_params.delete(:imported_from) - new_params.delete(:created_at) - new_params.delete(:updated_at) - - # spam checking is not necessary, as no new content is being created. - - # Skip creation of system notes for existing attributes of the issue when cloning with notes. - # The system notes of the old issue are copied over so we don't want to end up with duplicate notes. - # When cloning without notes, we want to generate system notes for the attributes that were copied. - create_result = CreateService.new( - container: target_project, - current_user: current_user, - params: new_params, - perform_spam_check: false - ).execute(skip_system_notes: with_notes) - - raise CloneError, create_result.errors.join(', ') if create_result.error? && create_result[:issue].blank? - - create_result[:issue] - end - - def queue_copy_designs - return unless original_entity.designs.present? - - response = DesignManagement::CopyDesignCollection::QueueService.new( - current_user, - original_entity, - new_entity - ).execute - - log_error(response.message) if response.error? - end - - def notify_participants - notification_service.async.issue_cloned(original_entity, new_entity, current_user) - end - - def add_note_from - SystemNoteService.noteable_cloned( - new_entity, - target_project, - original_entity, - current_user, - direction: :from, - created_at: new_entity.created_at - ) - end - - def add_note_to - SystemNoteService.noteable_cloned(original_entity, old_project, - new_entity, current_user, - direction: :to) - end - end -end - -Issues::CloneService.prepend_mod_with('Issues::CloneService') diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb deleted file mode 100644 index 421bbf3a43e..00000000000 --- a/app/services/issues/move_service.rb +++ /dev/null @@ -1,224 +0,0 @@ -# frozen_string_literal: true - -module Issues - class MoveService < Issuable::Clone::BaseService - extend ::Gitlab::Utils::Override - - BATCH_SIZE = 100 - - MoveError = Class.new(StandardError) - - def execute(issue, target_project, move_any_issue_type = false) - @move_any_issue_type = move_any_issue_type - @target_project = target_project - - verify_can_move_issue!(issue, target_project) - - super(issue, target_project) - - notify_participants - - # Updates old issue sent notifications allowing - # to receive service desk emails on the new moved issue. - update_service_desk_sent_notifications - - copy_email_participants - queue_copy_designs - copy_timelogs - - new_entity - end - - private - - attr_reader :target_project, :move_any_issue_type - - override :after_clone_actions - def after_clone_actions - move_children - end - - def move_children - WorkItems::ParentLink.for_parents(original_entity).each do |link| - new_child = self.class.new( - container: container, - current_user: current_user - ).execute( - ::Issue.find(link.work_item_id), - target_project, - true - ) - - WorkItems::ParentLink.create!(work_item_id: new_child.id, work_item_parent_id: new_entity.id) - end - end - - def verify_can_move_issue!(issue, target_project) - unless issue.supports_move_and_clone? || move_any_issue_type - raise MoveError, s_('MoveIssue|Cannot move issues of \'%{issue_type}\' type.') % { issue_type: issue.issue_type } - end - - unless issue.can_move?(current_user, @target_project) - raise MoveError, s_('MoveIssue|Cannot move issue due to insufficient permissions.') - end - - if @project == @target_project - raise MoveError, s_('MoveIssue|Cannot move issue to project it originates from.') - end - end - - def update_service_desk_sent_notifications - context = { project_id: new_entity.project_id, noteable_id: new_entity.id } - - original_entity.run_after_commit_or_now do - next unless from_service_desk? - - sent_notifications.update_all(**context) - end - end - - def copy_email_participants - new_attributes = { id: nil, issue_id: new_entity.id } - - new_participants = original_entity.issue_email_participants.dup - - new_participants.each do |participant| - participant.assign_attributes(new_attributes) - end - - IssueEmailParticipant.bulk_insert!(new_participants) - end - - override :update_old_entity - def update_old_entity - super - - recreate_related_issues - mark_as_moved - end - - override :update_new_entity - def update_new_entity - super - - copy_contacts - end - - def create_new_entity - new_params = { - id: nil, - iid: nil, - relative_position: relative_position, - project: target_project, - author: original_entity.author, - assignee_ids: original_entity.assignee_ids, - moved_issue: true, - imported_from: :none - } - - new_params = original_entity.serializable_hash.symbolize_keys.merge(new_params) - new_params = new_params.merge(rewritten_old_entity_attributes) - # spam checking is not necessary, as no new content is being created. - - # Skip creation of system notes for existing attributes of the issue. The system notes of the old - # issue are copied over so we don't want to end up with duplicate notes. - create_result = CreateService.new( - container: @target_project, - current_user: @current_user, - params: new_params, - perform_spam_check: false - ).execute(skip_system_notes: true) - - raise MoveError, create_result.errors.join(', ') if create_result.error? && create_result[:issue].blank? - - create_result[:issue] - end - - def queue_copy_designs - return unless original_entity.designs.present? - - response = DesignManagement::CopyDesignCollection::QueueService.new( - current_user, - original_entity, - new_entity - ).execute - - log_error(response.message) if response.error? - end - - def copy_timelogs - return if original_entity.timelogs.empty? - - WorkItems::CopyTimelogsWorker.perform_async(original_entity.id, new_entity.id) - end - - def mark_as_moved - original_entity.update(moved_to: new_entity) - end - - def recreate_related_issues - source_issue_links = IssueLink.for_source(original_entity) - target_issue_links = IssueLink.for_target(original_entity) - - source_issue_links.each_batch(of: BATCH_SIZE) do |links_batch| - new_links = new_links(links_batch, reference_attribute: 'source_id') - ::IssueLink.insert_all!(new_links) if new_links.any? - end - - target_issue_links.each_batch(of: BATCH_SIZE) do |links_batch| - new_links = new_links(links_batch, reference_attribute: 'target_id') - ::IssueLink.insert_all!(new_links) if new_links.any? - end - - source_issue_links.each_batch(of: BATCH_SIZE) do |links_batch| - links_batch.delete_all - end - - target_issue_links.each_batch(of: BATCH_SIZE) do |links_batch| - links_batch.delete_all - end - end - - def new_links(links_batch, reference_attribute:) - links_batch.map do |link| - link.attributes.except('id', 'namespace_id').merge(reference_attribute => new_entity.id) - end - end - - def copy_contacts - return unless original_entity.project.root_ancestor == new_entity.project.root_ancestor - - new_entity.customer_relations_contacts = original_entity.customer_relations_contacts - end - - def notify_participants - context = { original: original_entity, new: new_entity, user: @current_user, service: notification_service } - - original_entity.run_after_commit_or_now do - context[:service].async.issue_moved(context[:original], context[:new], context[:user]) - end - end - - def add_note_from - SystemNoteService.noteable_moved( - new_entity, - target_project, - original_entity, - current_user, - direction: :from - ) - end - - def add_note_to - SystemNoteService.noteable_moved( - original_entity, - old_project, - new_entity, - current_user, - direction: :to - ) - end - end -end - -Issues::MoveService.prepend_mod_with('Issues::MoveService') diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 0dda8163f41..f66b582d3da 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -103,16 +103,11 @@ module Issues update(issue) - if container.work_item_move_and_clone_flag_enabled? - move_service_container = target_container.is_a?(Project) ? target_container.project_namespace : target_container - ::WorkItems::DataSync::MoveService.new( - work_item: issue, current_user: current_user, target_namespace: move_service_container - ).execute[:work_item] - elsif target_container.is_a?(Project) || target_container.is_a?(Namespaces::ProjectNamespace) - ::Issues::MoveService.new( - container: project, current_user: current_user - ).execute(issue, target_container) - end + move_service_container = target_container.is_a?(Project) ? target_container.project_namespace : target_container + + ::WorkItems::DataSync::MoveService.new( + work_item: issue, current_user: current_user, target_namespace: move_service_container + ).execute[:work_item] end private @@ -157,17 +152,12 @@ module Issues # we've pre-empted this from running in #execute, so let's go ahead and update the Issue now. update(issue) - if container.work_item_move_and_clone_flag_enabled? - clone_service_container = target_container.is_a?(Project) ? target_container.project_namespace : target_container - ::WorkItems::DataSync::CloneService.new( - work_item: issue, current_user: current_user, target_namespace: clone_service_container, - params: { clone_with_notes: with_notes } - ).execute[:work_item] - elsif target_container.is_a?(Project) || target_container.is_a?(Namespaces::ProjectNamespace) - Issues::CloneService.new(container: project, current_user: current_user).execute( - issue, target_container, with_notes: with_notes - ) - end + clone_service_container = target_container.is_a?(Project) ? target_container.project_namespace : target_container + + ::WorkItems::DataSync::CloneService.new( + work_item: issue, current_user: current_user, target_namespace: clone_service_container, + params: { clone_with_notes: with_notes } + ).execute[:work_item] end def create_merge_request_from_quick_action diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index a071105bfdf..689a93b7213 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -65,6 +65,9 @@ = webpack_bundle_tag 'super_sidebar' + - if ENV['BABEL_ENV'] == 'istanbul' + = webpack_bundle_tag 'coverage_persistence' + - if vite_enabled? = render 'layouts/vite_main' - else diff --git a/babel.config.js b/babel.config.js index 9faaaeeae7a..4c7711dd4d6 100644 --- a/babel.config.js +++ b/babel.config.js @@ -27,6 +27,7 @@ const plugins = [ '@babel/plugin-transform-class-static-block', ]; +const env = {}; // Jest is running in node environment const isJest = Boolean(process.env.JEST_WORKER_ID); if (isJest) { @@ -40,6 +41,22 @@ if (isJest) { }, ], ]; +} else { + env.istanbul = { + plugins: [ + [ + 'istanbul', + { + extension: ['.js', '.vue', '.mjs', '.cjs'], + }, + ], + ], + }; } -module.exports = { presets, plugins, sourceType: 'unambiguous' }; +module.exports = { + presets, + plugins, + sourceType: 'unambiguous', + env, +}; diff --git a/config/feature_flags/beta/work_item_move_and_clone.yml b/config/feature_flags/beta/work_item_move_and_clone.yml deleted file mode 100644 index e2a7c172feb..00000000000 --- a/config/feature_flags/beta/work_item_move_and_clone.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: work_item_move_and_clone -feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/15470 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/179494 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/525524 -milestone: '17.10' -group: group::project management -type: beta -default_enabled: true diff --git a/config/feature_flags/development/allow_organization_creation.yml b/config/feature_flags/wip/allow_organization_creation.yml similarity index 76% rename from config/feature_flags/development/allow_organization_creation.yml rename to config/feature_flags/wip/allow_organization_creation.yml index ad36274d11c..79f19deda3e 100644 --- a/config/feature_flags/development/allow_organization_creation.yml +++ b/config/feature_flags/wip/allow_organization_creation.yml @@ -1,8 +1,9 @@ --- name: allow_organization_creation +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/441531 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147930 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/452062 milestone: '16.11' -type: development +type: wip group: group::organizations default_enabled: false diff --git a/config/feature_flags/wip/batch_load_linked_items.yml b/config/feature_flags/wip/batch_load_linked_items.yml new file mode 100644 index 00000000000..5a8e403804a --- /dev/null +++ b/config/feature_flags/wip/batch_load_linked_items.yml @@ -0,0 +1,9 @@ +--- +name: batch_load_linked_items +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/512056 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/189332 +rollout_issue_url: +milestone: '18.0' +group: group::product planning +type: wip +default_enabled: false diff --git a/config/feature_flags/development/edit_user_profile_vue.yml b/config/feature_flags/wip/edit_user_profile_vue.yml similarity index 75% rename from config/feature_flags/development/edit_user_profile_vue.yml rename to config/feature_flags/wip/edit_user_profile_vue.yml index 7aae8ef51cc..58af260323f 100644 --- a/config/feature_flags/development/edit_user_profile_vue.yml +++ b/config/feature_flags/wip/edit_user_profile_vue.yml @@ -1,8 +1,9 @@ --- name: edit_user_profile_vue +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/389918 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122402 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414552 milestone: '16.1' -type: development +type: wip group: group::organizations default_enabled: false diff --git a/config/feature_flags/development/explore_topics_cleaned_path.yml b/config/feature_flags/wip/explore_topics_cleaned_path.yml similarity index 73% rename from config/feature_flags/development/explore_topics_cleaned_path.yml rename to config/feature_flags/wip/explore_topics_cleaned_path.yml index d9529da537e..786f1db2878 100644 --- a/config/feature_flags/development/explore_topics_cleaned_path.yml +++ b/config/feature_flags/wip/explore_topics_cleaned_path.yml @@ -1,8 +1,9 @@ --- name: explore_topics_cleaned_path +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/393166 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122970 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414793 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414892 milestone: '16.1' -type: development +type: wip group: group::organizations default_enabled: false diff --git a/config/feature_flags/development/optional_personal_namespace.yml b/config/feature_flags/wip/optional_personal_namespace.yml similarity index 75% rename from config/feature_flags/development/optional_personal_namespace.yml rename to config/feature_flags/wip/optional_personal_namespace.yml index 56d087e4825..fefd3f5a018 100644 --- a/config/feature_flags/development/optional_personal_namespace.yml +++ b/config/feature_flags/wip/optional_personal_namespace.yml @@ -1,8 +1,9 @@ --- name: optional_personal_namespace +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427730 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137713 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/431978 milestone: '16.8' -type: development +type: wip group: group::organizations default_enabled: false diff --git a/config/feature_flags/development/profile_tabs_vue.yml b/config/feature_flags/wip/profile_tabs_vue.yml similarity index 75% rename from config/feature_flags/development/profile_tabs_vue.yml rename to config/feature_flags/wip/profile_tabs_vue.yml index eb4eca83e80..7f74a441e0b 100644 --- a/config/feature_flags/development/profile_tabs_vue.yml +++ b/config/feature_flags/wip/profile_tabs_vue.yml @@ -1,8 +1,9 @@ --- name: profile_tabs_vue +feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/9056 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109422 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/388708 milestone: '15.9' -type: development +type: wip group: group::organizations default_enabled: false diff --git a/config/feature_flags/development/ui_for_organizations.yml b/config/feature_flags/wip/ui_for_organizations.yml similarity index 75% rename from config/feature_flags/development/ui_for_organizations.yml rename to config/feature_flags/wip/ui_for_organizations.yml index d286d1fe348..d8d1e6aee0f 100644 --- a/config/feature_flags/development/ui_for_organizations.yml +++ b/config/feature_flags/wip/ui_for_organizations.yml @@ -1,8 +1,9 @@ --- name: ui_for_organizations +feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/17432 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122866 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/414592 milestone: '16.1' -type: development +type: wip group: group::organizations default_enabled: false diff --git a/config/webpack.config.js b/config/webpack.config.js index 9eb49f1b6d7..651e7362ac1 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -278,6 +278,7 @@ module.exports = { return { default: defaultEntries, sentry: './sentry/index.js', + coverage_persistence: './entrypoints/coverage_persistence.js', performance_bar: './entrypoints/performance_bar.js', jira_connect_app: './jira_connect/subscriptions/index.js', sandboxed_mermaid: './lib/mermaid.js', diff --git a/db/docs/batched_background_migrations/mark_admin_bot_runners_as_hosted.yml b/db/docs/batched_background_migrations/mark_admin_bot_runners_as_hosted.yml new file mode 100644 index 00000000000..358c4124bbe --- /dev/null +++ b/db/docs/batched_background_migrations/mark_admin_bot_runners_as_hosted.yml @@ -0,0 +1,8 @@ +--- +migration_job_name: MarkAdminBotRunnersAsHosted +description: Mark runners created by admin bot as hosted on GitLab Dedicated +feature_category: hosted_runners +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/183329 +milestone: '18.0' +queued_migration_version: 20250505095336 +finalized_by: # version of the migration that finalized this BBM diff --git a/db/post_migrate/20250505095336_queue_mark_admin_bot_runners_as_hosted.rb b/db/post_migrate/20250505095336_queue_mark_admin_bot_runners_as_hosted.rb new file mode 100644 index 00000000000..a1a2eacf4aa --- /dev/null +++ b/db/post_migrate/20250505095336_queue_mark_admin_bot_runners_as_hosted.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class QueueMarkAdminBotRunnersAsHosted < Gitlab::Database::Migration[2.3] + milestone '18.0' + + # Select the applicable gitlab schema for your batched background migration + restrict_gitlab_migration gitlab_schema: :gitlab_ci + + MIGRATION = "MarkAdminBotRunnersAsHosted" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 1000 + SUB_BATCH_SIZE = 100 + + def up + queue_batched_background_migration( + MIGRATION, + :ci_runners, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + delete_batched_background_migration(MIGRATION, :ci_runners, :id, []) + end +end diff --git a/db/schema_migrations/20250505095336 b/db/schema_migrations/20250505095336 new file mode 100644 index 00000000000..28ff1b5608b --- /dev/null +++ b/db/schema_migrations/20250505095336 @@ -0,0 +1 @@ +75166bbf5d04065b48b810200cada005c73109b8da8ccebc6ed9f3195951928a \ No newline at end of file diff --git a/doc/administration/gitaly/kubernetes.md b/doc/administration/gitaly/kubernetes.md index 918881117df..556c0e902e6 100644 --- a/doc/administration/gitaly/kubernetes.md +++ b/doc/administration/gitaly/kubernetes.md @@ -200,9 +200,9 @@ gitlab: memory: 32Mi ``` -#### Configure concurrency rate limiting +#### Configure concurrency limiting -As well as using cgroups, you can use concurrency limits to further help protect the service from abnormal traffic patterns. For more information, see +You can use concurrency limits to help protect the service from abnormal traffic patterns. For more information, see [concurrency configuration documentation](concurrency_limiting.md) and [how to monitor limits](monitoring.md#monitor-gitaly-concurrency-limiting). #### Isolate Gitaly pods diff --git a/doc/administration/gitaly/monitoring.md b/doc/administration/gitaly/monitoring.md index 84d4d366ecf..e5259bee59f 100644 --- a/doc/administration/gitaly/monitoring.md +++ b/doc/administration/gitaly/monitoring.md @@ -16,22 +16,19 @@ Metric definitions are available: -## Monitor Gitaly rate limiting (deprecated) +## Monitor Gitaly rate limiting (removed) {{< alert type="warning" >}} This feature was [deprecated](https://gitlab.com/gitlab-org/gitaly/-/issues/5011) in GitLab 17.7 -and is planned for removal in 18.0. Use [concurrency limiting](concurrency_limiting.md) instead. +and was removed in 18.0. Use [concurrency limiting](concurrency_limiting.md) instead. {{< /alert >}} -Gitaly can be configured to limit requests based on: - -- Concurrency of requests. -- A rate limit. - +Gitaly can be configured to limit requests based on concurrency of requests (adaptive or non-adaptive). + ## Monitor Gitaly concurrency limiting You can observe specific behavior of [concurrency-queued requests](concurrency_limiting.md#limit-rpc-concurrency) using Gitaly logs and Prometheus. diff --git a/doc/administration/gitlab_duo_self_hosted/_index.md b/doc/administration/gitlab_duo_self_hosted/_index.md index 2f74a307c21..22e13325cb9 100644 --- a/doc/administration/gitlab_duo_self_hosted/_index.md +++ b/doc/administration/gitlab_duo_self_hosted/_index.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: Custom Models info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Get started with GitLab Duo Self-Hosted. diff --git a/doc/administration/gitlab_duo_self_hosted/configuration_types.md b/doc/administration/gitlab_duo_self_hosted/configuration_types.md index 32ab99f22b9..21ef2ddf1b6 100644 --- a/doc/administration/gitlab_duo_self_hosted/configuration_types.md +++ b/doc/administration/gitlab_duo_self_hosted/configuration_types.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: Custom Models info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Get started with GitLab Duo Self-Hosted. diff --git a/doc/administration/gitlab_duo_self_hosted/configure_duo_features.md b/doc/administration/gitlab_duo_self_hosted/configure_duo_features.md index fd7bd8aa802..cc1e9a3362b 100644 --- a/doc/administration/gitlab_duo_self_hosted/configure_duo_features.md +++ b/doc/administration/gitlab_duo_self_hosted/configure_duo_features.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: Custom Models info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Configure your GitLab instance to use GitLab Duo Self-Hosted. diff --git a/doc/administration/gitlab_duo_self_hosted/logging.md b/doc/administration/gitlab_duo_self_hosted/logging.md index 7ca5efaf99e..53fe175bfd4 100644 --- a/doc/administration/gitlab_duo_self_hosted/logging.md +++ b/doc/administration/gitlab_duo_self_hosted/logging.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: Custom Models info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Enable logging for self-hosted models. diff --git a/doc/administration/gitlab_duo_self_hosted/supported_llm_serving_platforms.md b/doc/administration/gitlab_duo_self_hosted/supported_llm_serving_platforms.md index 6724e0af008..2c10b6abe41 100644 --- a/doc/administration/gitlab_duo_self_hosted/supported_llm_serving_platforms.md +++ b/doc/administration/gitlab_duo_self_hosted/supported_llm_serving_platforms.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: Custom Models info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Supported LLM Serving Platforms. diff --git a/doc/administration/gitlab_duo_self_hosted/supported_models_and_hardware_requirements.md b/doc/administration/gitlab_duo_self_hosted/supported_models_and_hardware_requirements.md index 1778c418292..5004498feea 100644 --- a/doc/administration/gitlab_duo_self_hosted/supported_models_and_hardware_requirements.md +++ b/doc/administration/gitlab_duo_self_hosted/supported_models_and_hardware_requirements.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: Custom Models info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Supported models and hardware requirements. diff --git a/doc/administration/gitlab_duo_self_hosted/troubleshooting.md b/doc/administration/gitlab_duo_self_hosted/troubleshooting.md index 0d15075c064..aca4f3d8e5f 100644 --- a/doc/administration/gitlab_duo_self_hosted/troubleshooting.md +++ b/doc/administration/gitlab_duo_self_hosted/troubleshooting.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: Custom Models info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Troubleshooting tips for deploying GitLab Duo Self-Hosted diff --git a/doc/administration/self_hosted_models/related_topics.md b/doc/administration/self_hosted_models/related_topics.md index 596e4c2541f..f2d64fc255b 100644 --- a/doc/administration/self_hosted_models/related_topics.md +++ b/doc/administration/self_hosted_models/related_topics.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: Custom Models info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Self-Hosted model setup Related topics diff --git a/doc/api/chat.md b/doc/api/chat.md index 629e7c11f36..fa3d1cf31ca 100644 --- a/doc/api/chat.md +++ b/doc/api/chat.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: Duo Chat info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Documentation for the REST API for Duo Chat. diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index bffd466ea5c..8db7eb70121 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -23156,7 +23156,7 @@ CI/CD variables for a project. | `ephemeralRegisterUrl` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 15.11. **Status**: Experiment. URL of the registration page of the runner manager. Only available for the creator of the runner for a limited time during registration. | | `groups` | [`GroupInterfaceConnection`](#groupinterfaceconnection) | Groups the runner is associated with. For group runners only. (see [Connections](#connections)) | | `id` | [`CiRunnerID!`](#cirunnerid) | ID of the runner. | -| `jobExecutionStatus` {{< icon name="warning-solid" >}} | [`CiRunnerJobExecutionStatus`](#cirunnerjobexecutionstatus) | **Introduced** in GitLab 15.7. **Status**: Experiment. Job execution status of the runner. | +| `jobExecutionStatus` | [`CiRunnerJobExecutionStatus`](#cirunnerjobexecutionstatus) | Job execution status of the runner. | | `locked` | [`Boolean`](#boolean) | Indicates the runner is locked. | | `maintenanceNote` | [`String`](#string) | Runner's maintenance notes. | | `maintenanceNoteHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `maintenance_note`. | @@ -23335,7 +23335,7 @@ Returns [`[CiRunnerCloudProvisioningStep!]`](#cirunnercloudprovisioningstep). | `executorName` | [`String`](#string) | Executor last advertised by the runner. | | `id` | [`CiRunnerManagerID!`](#cirunnermanagerid) | ID of the runner manager. | | `ipAddress` | [`String`](#string) | IP address of the runner manager. | -| `jobExecutionStatus` {{< icon name="warning-solid" >}} | [`CiRunnerJobExecutionStatus`](#cirunnerjobexecutionstatus) | **Introduced** in GitLab 16.3. **Status**: Experiment. Job execution status of the runner manager. | +| `jobExecutionStatus` | [`CiRunnerJobExecutionStatus`](#cirunnerjobexecutionstatus) | Job execution status of the runner manager. | | `platformName` | [`String`](#string) | Platform provided by the runner manager. | | `revision` | [`String`](#string) | Revision of the runner. | | `runner` | [`CiRunner`](#cirunner) | Runner configuration for the runner manager. | @@ -42827,8 +42827,8 @@ Runner cloud provider. | Value | Description | | ----- | ----------- | -| `ACTIVE` {{< icon name="warning-solid" >}} | **Introduced** in GitLab 17.2. **Status**: Experiment. Runner is busy. | -| `IDLE` {{< icon name="warning-solid" >}} | **Introduced** in GitLab 15.7. **Status**: Experiment. Runner is idle. | +| `ACTIVE` | Runner is busy. | +| `IDLE` | Runner is idle. | ### `CiRunnerMembershipFilter` @@ -48896,6 +48896,7 @@ Field that are available while modifying the custom mapping attributes for an HT | `or` | [`UnionedIssueFilterInput`](#unionedissuefilterinput) | List of arguments with inclusive OR. | | `releaseTag` | [`String`](#string) | Filter by release tag. | | `search` | [`String`](#string) | Search query for issue title or description. | +| `status` {{< icon name="warning-solid" >}} | [`WorkItemWidgetStatusFilterInput`](#workitemwidgetstatusfilterinput) | **Deprecated:** **Status**: Experiment. Introduced in GitLab 18.0. | | `types` | [`[IssueType!]`](#issuetype) | Filter by the given issue types. | | `weight` | [`String`](#string) | Filter by weight. | | `weightWildcardId` | [`WeightWildcardId`](#weightwildcardid) | Filter by weight ID wildcard. Incompatible with weight. | diff --git a/doc/development/secure_coding_guidelines.md b/doc/development/secure_coding_guidelines.md index aee819b50f8..4567e8e14bc 100644 --- a/doc/development/secure_coding_guidelines.md +++ b/doc/development/secure_coding_guidelines.md @@ -941,6 +941,44 @@ path.Clean("../../etc/passwd") // renders the path to "../../etc/passwd"; the file path will look back up to two parent directories! ``` +#### Safe File Operations in Go + +The Go standard library provides basic file operations like `os.Open`, `os.ReadFile`, `os.WriteFile`, and `os.Readlink`. However, these functions do not prevent path traversal attacks, where user-supplied paths can escape the intended directory and access sensitive system files. + +Example of unsafe usage: + +```go +// Vulnerable: user input is directly used in the path +os.Open(filepath.Join("/app/data", userInput)) +os.ReadFile(filepath.Join("/app/data", userInput)) +os.WriteFile(filepath.Join("/app/data", userInput), []byte("data"), 0644) +os.Readlink(filepath.Join("/app/data", userInput)) +``` + +To mitigate these risks, use the [`safeopen`](https://pkg.go.dev/github.com/google/safeopen) library functions. These functions enforce a secure root directory and sanitize file paths: + +Example of safe usage: + +```go +safeopen.OpenBeneath("/app/data", userInput) +safeopen.ReadFileBeneath("/app/data", userInput) +safeopen.WriteFileBeneath("/app/data", []byte("data"), 0644) +safeopen.ReadlinkBeneath("/app/data", userInput) +``` + +Benefits: + +- Prevents path traversal attacks (`../` sequences). +- Restricts file operations to trusted root directories. +- Secures against unauthorized file reads, writes, and symlink resolutions. +- Provides simple, developer-friendly replacements. + +References: + +- [Go Standard Library os Package](https://pkg.go.dev/os) +- [Safe Go Libraries Announcement](https://bughunters.google.com/blog/4925068200771584/the-family-of-safe-golang-libraries-is-growing) +- [OWASP Path Traversal Cheat Sheet](https://owasp.org/www-community/attacks/Path_Traversal) + ## OS command injection guidelines Command injection is an issue in which an attacker is able to execute arbitrary commands on the host diff --git a/doc/install/install_ai_gateway.md b/doc/install/install_ai_gateway.md index f0b9755a645..848ae16c08d 100644 --- a/doc/install/install_ai_gateway.md +++ b/doc/install/install_ai_gateway.md @@ -1,5 +1,5 @@ --- -stage: AI-Powered +stage: AI-powered group: AI Framework info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments description: Set up your self-hosted model GitLab AI gateway diff --git a/doc/integration/jira/_index.md b/doc/integration/jira/_index.md index 016a4cd4497..2e7ebed6e06 100644 --- a/doc/integration/jira/_index.md +++ b/doc/integration/jira/_index.md @@ -71,7 +71,7 @@ This table shows the features available with the Jira issues integration and the | [View a list of Jira issues](configure.md#view-jira-issues). | {{< icon name="check-circle" >}} Yes | {{< icon name="dotted-circle" >}} No | | [Create a Jira issue for a vulnerability](configure.md#create-a-jira-issue-for-a-vulnerability). | {{< icon name="check-circle" >}} Yes | {{< icon name="dotted-circle" >}} No | | Create a GitLab branch from a Jira issue. | {{< icon name="dotted-circle" >}} No | {{< icon name="check-circle" >}} Yes, in the Jira issue's development panel. | -| Mention a Jira issue ID in a GitLab merge request, branch name, or any of the last 5,000 commits to the branch after the last successful deployment to the environment to sync a GitLab deployment to a Jira issue. | {{< icon name="dotted-circle" >}} No | {{< icon name="check-circle" >}} Yes, in the Jira issue's development panel. | +| Mention a Jira issue ID in a GitLab merge request, branch name, or any of the last 2,000 commits to the branch after the last successful deployment to the environment to sync a GitLab deployment to a Jira issue. | {{< icon name="dotted-circle" >}} No | {{< icon name="check-circle" >}} Yes, in the Jira issue's development panel. | ## Privacy considerations diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md index 100ca67a665..7b3f3ad0ebc 100644 --- a/doc/integration/jira/development_panel.md +++ b/doc/integration/jira/development_panel.md @@ -66,7 +66,7 @@ For the [GitLab for Jira Cloud app](connect-app.md), the following information i |---------------------------------------------|-------------------------------------------------------| | Merge request title or description | Link to the merge request
    Link to the deployment
    Link to the pipeline through the merge request title
    Link to the pipeline through the merge request description ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390888) in GitLab 15.10)
    Link to the branch ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354373) in GitLab 15.11)
    Reviewer information and approval status ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/364273) in GitLab 16.5) | | Branch name | Link to the branch
    Link to the deployment | -| Commit message | Link to the commit
    Link to the deployment from up to 5,000 commits after the last successful deployment to the environment 1 2 | +| Commit message | Link to the commit
    Link to the deployment from up to 2,000 commits after the last successful deployment to the environment 1 2 | | [Jira Smart Commit](#jira-smart-commits) | Custom comment, logged time, or workflow transition | **Footnotes:** diff --git a/doc/user/application_security/dependency_list/_index.md b/doc/user/application_security/dependency_list/_index.md index 67cc768cd83..2dac5753e72 100644 --- a/doc/user/application_security/dependency_list/_index.md +++ b/doc/user/application_security/dependency_list/_index.md @@ -79,7 +79,7 @@ Details of each dependency are listed, sorted by decreasing severity of vulnerab | Packager | The packager used to install the dependency. | | Location | For system dependencies, this field lists the image that was scanned. For application dependencies, this field shows a link to the packager-specific lock file in your project that declared the dependency. It also shows the direct [dependents](#dependency-paths), if any. If there are transitive dependencies, selecting **View dependency paths** shows the full path of all dependents. Transitive dependencies are indirect dependents that have a direct dependent as an ancestor. | | License (for projects only) | Links to dependency's software licenses. A warning badge that includes the number of vulnerabilities detected in the dependency. | -| Projects (for groups only) | Links to the project with the dependency. If multiple projects have the same dependency, the total number of these projects is shown. To go to a project with this dependency, select the **Projects** number, then search for and select its name. The project search feature is supported only on groups that have up to 600 occurrences in their group hierarchy. | +| Projects (for groups only) | Links to the project with the dependency. If multiple projects have the same dependency, the total number of these projects is shown. To go to a project with this dependency, select the **Projects** number, then search for and select its name. | ## Filter dependency list diff --git a/doc/user/gitlab_duo_chat/troubleshooting.md b/doc/user/gitlab_duo_chat/troubleshooting.md index 973fe49eacc..da5acefaa48 100644 --- a/doc/user/gitlab_duo_chat/troubleshooting.md +++ b/doc/user/gitlab_duo_chat/troubleshooting.md @@ -25,6 +25,7 @@ If this does not work, you can also check the following troubleshooting document - [Microsoft Visual Studio](../../editor_extensions/visual_studio/visual_studio_troubleshooting.md). - [JetBrains IDEs](../../editor_extensions/jetbrains_ide/jetbrains_troubleshooting.md). - [Neovim](../../editor_extensions/neovim/neovim_troubleshooting.md). +- [Eclipse](../../editor_extensions/eclipse/troubleshooting.md). - [Troubleshooting GitLab Duo](../gitlab_duo/troubleshooting.md). - [Troubleshooting GitLab Duo Self-Hosted](../../administration/gitlab_duo_self_hosted/troubleshooting.md). diff --git a/gems/gem-pg.gitlab-ci.yml b/gems/gem-pg.gitlab-ci.yml index 7984fc6099b..633c92d0621 100644 --- a/gems/gem-pg.gitlab-ci.yml +++ b/gems/gem-pg.gitlab-ci.yml @@ -53,12 +53,8 @@ default: - bundle config # Show bundler configuration - bundle install --jobs=$(nproc) --retry=3 after_script: - # We need this at the very top, because the section_start/section_end logic is defined there. - source $CI_PROJECT_DIR/scripts/utils.sh - - | - section_start "failure-analyzer" "Report failure category" - $CI_PROJECT_DIR/tooling/lib/tooling/glci/failure_analyzer.rb $CI_JOB_ID || true - section_end "failure-analyzer" + - execute_failure_analyzer .ruby_matrix: parallel: diff --git a/gems/gem.gitlab-ci.yml b/gems/gem.gitlab-ci.yml index 4ec5a9a3bec..c54aa4a4f7b 100644 --- a/gems/gem.gitlab-ci.yml +++ b/gems/gem.gitlab-ci.yml @@ -53,12 +53,8 @@ include: - bundle config # Show bundler configuration - bundle install --jobs=$(nproc) --retry=3 after_script: - # We need this at the very top, because the section_start/section_end logic is defined there. - source $CI_PROJECT_DIR/scripts/utils.sh - - | - section_start "failure-analyzer" "Report failure category" - $CI_PROJECT_DIR/tooling/lib/tooling/glci/failure_analyzer.rb $CI_JOB_ID || true - section_end "failure-analyzer" + - execute_failure_analyzer default: <<: *default diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 2c37ac298ca..09d9dd59ab9 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -381,23 +381,15 @@ module API not_found!('Project') unless new_project begin - issue = if user_project.work_item_move_and_clone_flag_enabled? - response = ::WorkItems::DataSync::MoveService.new( - work_item: issue, current_user: current_user, target_namespace: new_project.project_namespace - ).execute + response = ::WorkItems::DataSync::MoveService.new( + work_item: issue, current_user: current_user, target_namespace: new_project.project_namespace + ).execute - render_api_error!(response.message, 400) if response.error? + render_api_error!(response.message, 400) if response.error? - response.payload[:work_item] - else - ::Issues::MoveService.new( - container: user_project, current_user: current_user - ).execute(issue, new_project) - end + issue = response.payload[:work_item] present issue, with: Entities::Issue, current_user: current_user, project: user_project - rescue ::Issues::MoveService::MoveError => error - render_api_error!(error.message, 400) end end # rubocop: enable CodeReuse/ActiveRecord @@ -421,23 +413,16 @@ module API not_found!('Project') unless target_project begin - issue = if user_project.work_item_move_and_clone_flag_enabled? - response = ::WorkItems::DataSync::CloneService.new( - work_item: issue, current_user: current_user, target_namespace: target_project.project_namespace, - params: { clone_with_notes: params[:with_notes] } - ).execute + response = ::WorkItems::DataSync::CloneService.new( + work_item: issue, current_user: current_user, target_namespace: target_project.project_namespace, + params: { clone_with_notes: params[:with_notes] } + ).execute - render_api_error!(response.message, 400) if response.error? + render_api_error!(response.message, 400) if response.error? - response.payload[:work_item] - else - ::Issues::CloneService.new(container: user_project, current_user: current_user) - .execute(issue, target_project, with_notes: params[:with_notes]) - end + issue = response.payload[:work_item] present issue, with: Entities::Issue, current_user: current_user, project: target_project - rescue ::Issues::CloneService::CloneError => error - render_api_error!(error.message, 400) end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/atlassian/jira_connect/serializers/deployment_entity.rb b/lib/atlassian/jira_connect/serializers/deployment_entity.rb index 37066dcaffb..17357b73a96 100644 --- a/lib/atlassian/jira_connect/serializers/deployment_entity.rb +++ b/lib/atlassian/jira_connect/serializers/deployment_entity.rb @@ -7,7 +7,8 @@ module Atlassian include Gitlab::Routing include Gitlab::Utils::StrongMemoize - COMMITS_LIMIT = 5_000 + COMMITS_LIMIT = 2000 + ISSUE_KEY_LIMIT = 500 format_with(:iso8601, &:iso8601) @@ -26,7 +27,8 @@ module Atlassian expose :generate_deployment_commands_from_integration_configuration, as: :commands def issue_keys - @issue_keys ||= (issue_keys_from_pipeline + issue_keys_from_commits_since_last_deploy).uniq + @issue_keys ||= (issue_keys_from_pipeline + issue_keys_from_commits_since_last_deploy) + .uniq.first(ISSUE_KEY_LIMIT) end def associations @@ -109,20 +111,21 @@ module Atlassian .successful_deployments .id_not_in(deployment.id) .ordered - .find_by_ref(deployment.ref) + .first &.commit + commit_range = if last_deployed_commit + "#{last_deployed_commit.id}..#{deployment.commit.id}" + else + deployment.commit.id + end + commits = project.repository.commits( - deployment.ref, - before: deployment.commit.created_at, - after: last_deployed_commit&.created_at, + commit_range, skip_merges: true, limit: COMMITS_LIMIT ) - # Include this deploy's commit, as the `before:` param in `Repository#list_commits_by` excluded it. - commits << deployment.commit - commits.flat_map do |commit| JiraIssueKeyExtractor.new(commit.message).issue_keys end.compact diff --git a/lib/gitlab/background_migration/mark_admin_bot_runners_as_hosted.rb b/lib/gitlab/background_migration/mark_admin_bot_runners_as_hosted.rb new file mode 100644 index 00000000000..edf5cc61aee --- /dev/null +++ b/lib/gitlab/background_migration/mark_admin_bot_runners_as_hosted.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # This class doesn't create SecuritySetting + # as this feature exists only in EE + class MarkAdminBotRunnersAsHosted < BatchedMigrationJob + feature_category :hosted_runners + + def perform; end + end + end +end + +# rubocop:disable Layout/LineLength -- If I do multiline, another cop complains about prepend should be last line +Gitlab::BackgroundMigration::MarkAdminBotRunnersAsHosted.prepend_mod_with('Gitlab::BackgroundMigration::MarkAdminBotRunnersAsHosted') +# rubocop:enable Layout/LineLength diff --git a/lib/gitlab/quick_actions/issue_actions.rb b/lib/gitlab/quick_actions/issue_actions.rb index 4f1b5310eea..88c4e1ea5ab 100644 --- a/lib/gitlab/quick_actions/issue_actions.rb +++ b/lib/gitlab/quick_actions/issue_actions.rb @@ -109,8 +109,7 @@ module Gitlab types Issue, WorkItem condition do quick_action_target.persisted? && - current_user.can?(:"clone_#{quick_action_target.to_ability_name}", quick_action_target) && - can_be_moved_or_cloned? + current_user.can?(:"clone_#{quick_action_target.to_ability_name}", quick_action_target) end command :clone do |params = ''| params = params.split(' ') @@ -147,8 +146,7 @@ module Gitlab types Issue, WorkItem condition do quick_action_target.persisted? && - current_user.can?(:"move_#{quick_action_target.to_ability_name}", quick_action_target) && - can_be_moved_or_cloned? + current_user.can?(:"move_#{quick_action_target.to_ability_name}", quick_action_target) end command :move do |target_container_path| target_container = fetch_target_container(target_container_path) @@ -449,12 +447,6 @@ module Gitlab Group.find_by_full_path(target_container_path) end end - - def can_be_moved_or_cloned? - return true unless quick_action_target.is_a?(WorkItem) && quick_action_target.work_item_type.epic? - - container.work_item_move_and_clone_flag_enabled? - end end end end diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb index db11106bf0d..c9aa047310a 100644 --- a/lib/gitlab/quick_actions/merge_request_actions.rb +++ b/lib/gitlab/quick_actions/merge_request_actions.rb @@ -304,8 +304,20 @@ module Gitlab if reviewers.blank? _("Failed to assign a reviewer because no user was specified.") else - _('Assigned %{reviewer_users_sentence} as %{reviewer_text}.') % { reviewer_users_sentence: reviewer_users_sentence(users), - reviewer_text: 'reviewer'.pluralize(reviewers.size) } + processed_users = process_reviewer_users(users) + processed_msg = process_reviewer_users_message + + if processed_users.present? + [ + processed_msg, + _('Assigned %{reviewer_users_sentence} as %{reviewer_text}.') % { + reviewer_users_sentence: reviewer_users_sentence(processed_users), + reviewer_text: 'reviewer'.pluralize(processed_users.size) + } + ].compact.join(' ') + else + processed_msg + end end end params do @@ -319,13 +331,15 @@ module Gitlab extract_users(reviewer_param) end command :assign_reviewer, :reviewer do |users| - next if users.empty? + processed_users = process_reviewer_users(users) + + next if processed_users.empty? if quick_action_target.allows_multiple_reviewers? @updates[:reviewer_ids] ||= quick_action_target.reviewers.map(&:id) - @updates[:reviewer_ids] |= users.map(&:id) + @updates[:reviewer_ids] |= processed_users.map(&:id) else - @updates[:reviewer_ids] = [users.first.id] + @updates[:reviewer_ids] = [processed_users.first.id] end end @@ -347,7 +361,19 @@ module Gitlab if users.blank? _("Failed to request a review because no user was specified.") else - _('Requested a review from %{reviewer_users_sentence}.') % { reviewer_users_sentence: reviewer_users_sentence(users) } + processed_users = process_reviewer_users(users) + processed_msg = process_reviewer_users_message + + if processed_users.present? + [ + processed_msg, + _('Requested a review from %{reviewer_users_sentence}.') % { + reviewer_users_sentence: reviewer_users_sentence(processed_users) + } + ].compact.join(' ') + else + processed_msg + end end end params do @@ -361,7 +387,9 @@ module Gitlab extract_users(reviewer_param) end command :request_review do |users| - next if users.empty? + processed_users = process_reviewer_users(users) + + next if processed_users.empty? @updates[:reviewer_ids] ||= quick_action_target.reviewers.map(&:id) @@ -370,7 +398,7 @@ module Gitlab current_user: current_user ) - reviewers_to_add(users).each do |user| + reviewers_to_add(processed_users).each do |user| if @updates[:reviewer_ids].include?(user.id) # Request a new review from the reviewer if they are already assigned service.execute(quick_action_target, user) @@ -479,6 +507,16 @@ module Gitlab def preferred_auto_merge_strategy(merge_request) merge_orchestration_service.preferred_auto_merge_strategy(merge_request) end + + # Overriden in EE + def process_reviewer_users(users) + users + end + + # Overriden in EE + def process_reviewer_users_message + nil + end end end end diff --git a/lib/gitlab/slash_commands/issue_move.rb b/lib/gitlab/slash_commands/issue_move.rb index ff5910e3b7b..2fa3bcee6c6 100644 --- a/lib/gitlab/slash_commands/issue_move.rb +++ b/lib/gitlab/slash_commands/issue_move.rb @@ -29,24 +29,16 @@ module Gitlab return Gitlab::SlashCommands::Presenters::Access.new.not_found end - new_issue = if project.work_item_move_and_clone_flag_enabled? - response = ::WorkItems::DataSync::MoveService.new( - work_item: old_issue, current_user: current_user, - target_namespace: target_project.project_namespace - ).execute + response = ::WorkItems::DataSync::MoveService.new( + work_item: old_issue, current_user: current_user, + target_namespace: target_project.project_namespace + ).execute - return presenter(old_issue).display_move_error(response.message) if response.error? + return presenter(old_issue).display_move_error(response.message) if response.error? - response[:work_item] - else - ::Issues::MoveService.new( - container: project, current_user: current_user - ).execute(old_issue, target_project) - end + new_issue = response[:work_item] presenter(new_issue).present(old_issue) - rescue ::Issues::MoveService::MoveError => e - presenter(old_issue).display_move_error(e.message) end private diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c9488a467e2..d8d0fdedd95 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13232,15 +13232,6 @@ msgstr "" msgid "Clone with SSH" msgstr "" -msgid "CloneIssue|Cannot clone issue due to insufficient permissions." -msgstr "" - -msgid "CloneIssue|Cannot clone issue to target project as it is pending deletion." -msgstr "" - -msgid "CloneIssue|Cannot clone issues of '%{issue_type}' type." -msgstr "" - msgid "CloneWorkItem|Unable to clone. Cloning '%{work_item_type}' is not supported." msgstr "" @@ -21024,9 +21015,6 @@ msgstr "" msgid "Dependencies|Export as JSON" msgstr "" -msgid "Dependencies|Filtering unavailable" -msgstr "" - msgid "Dependencies|Follow the link below to download the export." msgstr "" @@ -21051,9 +21039,6 @@ msgstr "" msgid "Dependencies|Packager" msgstr "" -msgid "Dependencies|Project list unavailable" -msgstr "" - msgid "Dependencies|Projects" msgstr "" @@ -21090,12 +21075,6 @@ msgstr "" msgid "Dependencies|There was an error fetching the versions for the selected component. Please try again later." msgstr "" -msgid "Dependencies|This group exceeds the maximum number of 600 sub-groups. We cannot accurately filter or search the dependency list above this maximum. To view or filter a subset of this information, go to a subgroup's dependency list." -msgstr "" - -msgid "Dependencies|This group exceeds the maximum number of sub-groups of 600. We cannot accurately display a project list at this time. Please access a sub-group dependency list to view this information or see the %{linkStart}dependency list help %{linkEnd} page to learn more." -msgstr "" - msgid "Dependencies|This link will expire in %{number} days." msgstr "" @@ -23041,6 +23020,9 @@ msgstr "" msgid "DuoCodeReview|I've received your Duo Code Review request, and will review your code shortly." msgstr "" +msgid "DuoCodeReview|Your account doesn't have GitLab Duo access. Please contact your system administrator for access." +msgstr "" + msgid "DuoCodeReview|is reviewing your merge request and will let you know when it's finished" msgstr "" @@ -39128,15 +39110,6 @@ msgstr "" msgid "Move up" msgstr "" -msgid "MoveIssue|Cannot move issue due to insufficient permissions." -msgstr "" - -msgid "MoveIssue|Cannot move issue to project it originates from." -msgstr "" - -msgid "MoveIssue|Cannot move issues of '%{issue_type}' type." -msgstr "" - msgid "MoveWorkItem|Unable to move. Moving '%{work_item_type}' is not supported." msgstr "" @@ -40459,6 +40432,9 @@ msgstr "" msgid "Non-admin users are restricted to read-only access, in both GitLab UI and API." msgstr "" +msgid "Non-archived" +msgstr "" + msgid "None" msgstr "" @@ -46951,6 +46927,9 @@ msgstr "" msgid "Project ID" msgstr "" +msgid "Project Status" +msgstr "" + msgid "Project Templates" msgstr "" diff --git a/package.json b/package.json index fdce1e0f232..e3a275996b3 100644 --- a/package.json +++ b/package.json @@ -270,6 +270,7 @@ "ajv-formats": "^2.1.1", "axios-mock-adapter": "^1.15.0", "babel-jest": "^29.7.0", + "babel-plugin-istanbul": "^7.0.0", "chalk": "^2.4.1", "chokidar": "^3.5.3", "crypto": "^1.0.1", diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 7e951e0a025..b00110dc968 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -76,12 +76,8 @@ module QA click_element 'new-file-menu-item' end - # Click by JS is needed to bypass the VSCode Web IDE popover - # Change back to regular click_element when vscode_web_ide FF is removed - # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/371084 def fork_project - fork_button = find_element('fork-button') - click_by_javascript(fork_button) + click_element 'fork-button' end def forked_from?(parent_project_name) diff --git a/qa/qa/page/project/web_ide/vscode.rb b/qa/qa/page/project/web_ide/vscode.rb index 1043dad348d..268f612025b 100644 --- a/qa/qa/page/project/web_ide/vscode.rb +++ b/qa/qa/page/project/web_ide/vscode.rb @@ -167,7 +167,7 @@ module QA end end - # Used for stability, due to feature_caching of vscode_web_ide + # Used for stability # @param file_name [string] wait for file to be loaded (optional) def wait_for_ide_to_load(file_name = nil) page.driver.browser.switch_to.window(page.driver.browser.window_handles.last) diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index e98a6edede2..8f07367a962 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -98,6 +98,10 @@ module QA enabled?(ENV['COVERBAND_ENABLED'], default: false) end + def istanbul_coverage_enabled? + ENV['BABEL_ENV'] == 'istanbul' + end + def selective_execution_improved_enabled? enabled?(ENV['SELECTIVE_EXECUTION_IMPROVED'], default: false) end diff --git a/qa/qa/specs/spec_helper.rb b/qa/qa/specs/spec_helper.rb index 3c291df7562..4072c0273b7 100644 --- a/qa/qa/specs/spec_helper.rb +++ b/qa/qa/specs/spec_helper.rb @@ -26,6 +26,20 @@ RSpec.configure(&:disable_monkey_patching!) # For JH additionally process when `jh/` exists require_relative('../../../jh/qa/qa/specs/spec_helper') if GitlabEdition.jh? +front_end_coverage_by_example = {} + +def save_front_end_coverage_mapping(map_to_save) + return if map_to_save.empty? + + file = "tmp/js-coverage-by-example-#{ENV['CI_JOB_NAME_SLUG'] || 'local'}-#{SecureRandom.hex(6)}.json" + + # Write the mapping data + File.write(file, map_to_save.to_json) + QA::Runtime::Logger.info("Saved test coverage mapping data to #{file}") +rescue StandardError => e + QA::Runtime::Logger.error("Failed to save JS coverage mapping data, error: #{e}") +end + RSpec.configure do |config| config.include ActiveSupport::Testing::TimeHelpers config.include QA::Support::Matchers::EventuallyMatcher @@ -53,6 +67,11 @@ RSpec.configure do |config| visit(QA::Runtime::Scenario.gitlab_address) if QA::Runtime::Env.mobile_layout? + # Reset coverage persistence at the start of each test + if Capybara::Session.instance_created? && QA::Runtime::Env.istanbul_coverage_enabled? + Capybara.current_session.execute_script("window.__coveragePathsPersistence.reset()") + end + # Reset fabrication counters tracked in resource base Thread.current[:api_fabrication] = 0 Thread.current[:browser_ui_fabrication] = 0 @@ -78,6 +97,19 @@ RSpec.configure do |config| QA::Support::PageErrorChecker.log_request_errors(page) QA::Support::PageErrorChecker.check_page_for_error_code(page) if example.exception end + # Get coverage paths and store in metadata + if Capybara::Session.instance_created? && QA::Runtime::Env.istanbul_coverage_enabled? + begin + Capybara.current_session.execute_script("window.__coveragePathsPersistence.update()") + coverage_paths = Capybara.current_session.evaluate_script("window.__coveragePathsPersistence.getPaths()") + QA::Runtime::Logger.debug("Coverage paths count: #{coverage_paths.length}") + + example.metadata[:coverage_paths] = coverage_paths + front_end_coverage_by_example[example.metadata[:location]] = coverage_paths + rescue StandardError => e + QA::Runtime::Logger.warn("Failed to collect coverage paths: #{e.message}") + end + end end config.append_after do |example| @@ -98,6 +130,8 @@ RSpec.configure do |config| config.after(:suite) do |suite| # Write all test created resources to JSON file QA::Tools::TestResourceDataProcessor.write_to_file(suite.reporter.failed_examples.any?) + + save_front_end_coverage_mapping(front_end_coverage_by_example) if QA::Runtime::Env.istanbul_coverage_enabled? end config.expect_with :rspec do |expectations| diff --git a/scripts/utils.sh b/scripts/utils.sh index 3da24d2fa3e..ecfef24c93a 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -718,3 +718,13 @@ JSON -H 'Content-type: application/json' \ -d "${json_payload}" } + +function execute_failure_analyzer() { + # IMPORTANT - If you want to change the "failure-analyzer" string, + # please also change the logic for the failure categories, as we rely on this marker. + # + # Class relying on the marker: tooling/lib/tooling/glci/failure_categories/download_job_trace.rb + section_start "failure-analyzer" "Report failure category" + $CI_PROJECT_DIR/tooling/lib/tooling/glci/failure_analyzer.rb $CI_JOB_ID || true + section_end "failure-analyzer" +} diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 0617a536abd..520f163dab7 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -545,21 +545,7 @@ RSpec.describe Projects::IssuesController, :request_store, feature_category: :te end end - context 'with work_item_move_and_clone disabled' do - it_behaves_like 'move issue request' do - before do - stub_feature_flags(work_item_move_and_clone: false) - end - end - end - - context 'with work_item_move_and_clone enabled' do - it_behaves_like 'move issue request' do - before do - stub_feature_flags(work_item_move_and_clone: true) - end - end - end + it_behaves_like 'move issue request' end describe 'PUT #reorder' do diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb index a8a56ffe310..1b9ab266781 100644 --- a/spec/features/ide/user_opens_merge_request_spec.rb +++ b/spec/features/ide/user_opens_merge_request_spec.rb @@ -3,13 +3,13 @@ require 'spec_helper' RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do + include Features::WebIdeSpecHelpers + let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public, :repository, namespace: user.namespace) } let_it_be(:merge_request) { create(:merge_request, :simple, source_project: project) } before do - stub_feature_flags(vscode_web_ide: false) - sign_in(user) visit(merge_request_path(merge_request)) @@ -19,10 +19,16 @@ RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do within '.merge-request' do click_button 'Code' end - click_link 'Open in Web IDE' + new_tab = window_opened_by { click_link 'Open in Web IDE' } + + switch_to_window new_tab wait_for_requests - expect(page).not_to have_selector('.monaco-diff-editor') + within_window new_tab do + within_web_ide do + expect(page).to have_css('a[aria-label^="Next Change"]') + end + end end end diff --git a/spec/frontend/lib/utils/mock_data.js b/spec/frontend/lib/utils/mock_data.js index 06680c8f107..3a9e14c94ef 100644 --- a/spec/frontend/lib/utils/mock_data.js +++ b/spec/frontend/lib/utils/mock_data.js @@ -81,18 +81,24 @@ export const secretDetectionFindings = [ { message: 'Hello world! glpat-mGYFaXBmNLvLmrEb7xdf', type: 'GitLab personal access token', - redactedString: 'glpat*****************7xdf', + secret: 'glpat-mGYFaXBmNLvLmrEb7xdf', }, { message: 'Second token: gldt-cgyKc1k_AsnEpmP-5fRL', type: 'GitLab Deploy Token', - redactedString: 'gldt-****************5fRL', + secret: 'gldt-cgyKc1k_AsnEpmP-5fRL', }, { - message: 'third token: feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ', + message: 'Third token: feed_token=ABCDEFGHIJKLMNOPQRSTUVWXYZ', type: 'Feed Token', - redactedString: 'feed_**********************QRST', + secret: 'feed_token=ABCDEFGHIJKLMNOPQRST', + }, + + { + message: 'Repeated token: glpat-mGYFaXBmNLvLmrEb7xdf glpat-mGYFaXBmNLvLmrEb7xdf', + type: 'GitLab personal access token', + secret: 'glpat-mGYFaXBmNLvLmrEb7xdf', }, ]; diff --git a/spec/frontend/lib/utils/secret_detection_spec.js b/spec/frontend/lib/utils/secret_detection_spec.js index 9e7e23177e0..d5a9b4f5e5f 100644 --- a/spec/frontend/lib/utils/secret_detection_spec.js +++ b/spec/frontend/lib/utils/secret_detection_spec.js @@ -115,15 +115,15 @@ describe('detectAndConfirmSensitiveTokens', () => { modalHtmlMessage: expect.any(String), }; - describe('with single findings', () => { - const [{ message, type, redactedString }] = findings; + describe('with single finding', () => { + const [{ message, type, secret }] = findings; it('should call confirmAction with correct parameters', async () => { await detectAndConfirmSensitiveTokens({ content: message }); const confirmActionArgs = confirmAction.mock.calls[0][1]; expect(confirmActionArgs).toMatchObject(baseConfirmActionParams); expect(confirmActionArgs.title).toBe('Warning: Potential secret detected'); - expect(confirmActionArgs.modalHtmlMessage).toContain(`${type}: ${redactedString}`); + expect(confirmActionArgs.modalHtmlMessage).toContain(`${type}: ${secret}`); }); }); @@ -137,12 +137,28 @@ describe('detectAndConfirmSensitiveTokens', () => { expect(confirmActionArgs).toMatchObject(baseConfirmActionParams); expect(confirmActionArgs.title).toBe('Warning: Potential secrets detected'); - findings.forEach(({ type, redactedString }) => { - expect(confirmActionArgs.modalHtmlMessage).toContain(`${type}: ${redactedString}`); + findings.forEach(({ type, secret }) => { + expect(confirmActionArgs.modalHtmlMessage).toContain(`${type}: ${secret}`); }); }); }); + describe('with repeated finding', () => { + const { message, type, secret } = findings.at(-1); + it('should call confirmAction with correct parameters', async () => { + await detectAndConfirmSensitiveTokens({ content: message }); + + const confirmActionArgs = confirmAction.mock.calls[0][1]; + const stringToMatch = `${type}: ${secret}`; + expect(confirmActionArgs).toMatchObject(baseConfirmActionParams); + expect(confirmActionArgs.title).toBe('Warning: Potential secret detected'); + const tokenRegex = new RegExp(stringToMatch, 'g'); + const matches = confirmActionArgs.modalHtmlMessage.match(tokenRegex); + + expect(matches).toHaveLength(1); + }); + }); + describe('with different content type', () => { const testCases = [ [ diff --git a/spec/graphql/mutations/issues/move_spec.rb b/spec/graphql/mutations/issues/move_spec.rb index dd79b317a5f..13fd9e920ea 100644 --- a/spec/graphql/mutations/issues/move_spec.rb +++ b/spec/graphql/mutations/issues/move_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Mutations::Issues::Move, feature_category: :api do it 'returns error message' do expect(resolve[:issue]).to eq(nil) - expect(resolve[:errors].first).to eq(permissions_error_message) + expect(resolve[:errors].first).to eq("Unable to move. You have insufficient permissions.") end end @@ -43,23 +43,5 @@ RSpec.describe Mutations::Issues::Move, feature_category: :api do end end - context 'with work_item_move_and_clone disabled' do - it_behaves_like 'moving work item mutation' do - let(:permissions_error_message) { "Cannot move issue due to insufficient permissions." } - - before do - stub_feature_flags(work_item_move_and_clone: false) - end - end - end - - context 'with work_item_move_and_clone enabled' do - it_behaves_like 'moving work item mutation' do - let(:permissions_error_message) { "Unable to move. You have insufficient permissions." } - - before do - stub_feature_flags(work_item_move_and_clone: true) - end - end - end + it_behaves_like 'moving work item mutation' end diff --git a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb index 47668d83a10..4fc31339aea 100644 --- a/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb +++ b/spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb @@ -330,7 +330,9 @@ RSpec.describe Atlassian::JiraConnect::Serializers::DeploymentEntity, feature_ca last_deploy.update!(ref: 'foo') end - it_behaves_like 'ignores that deployment' + it 'extracts issue keys from commits made since the last deploy regardless of ref' do + expect(subject.issue_keys).to contain_exactly('add a', 'add d') + end end context 'when the deploy was not successful' do diff --git a/spec/lib/gitlab/doctor/encryption_keys_spec.rb b/spec/lib/gitlab/doctor/encryption_keys_spec.rb index 6e0ccf0745f..c4b44be85b2 100644 --- a/spec/lib/gitlab/doctor/encryption_keys_spec.rb +++ b/spec/lib/gitlab/doctor/encryption_keys_spec.rb @@ -18,33 +18,30 @@ RSpec.describe Gitlab::Doctor::EncryptionKeys, feature_category: :shared do context 'when no encrypted attributes exist' do it 'outputs "NONE"' do - expect(logger).to receive(:info).with(/Encryption keys usage for DependencyProxy::GroupSetting: NONE/) + expect(logger).to receive(:info).with("Encryption keys usage for DependencyProxy::GroupSetting: NONE") doctor_encryption_secrets end end context 'when encrypted attributes exist' do - # This will work in Rails 7.1.4, see https://github.com/rails/rails/issues/52003#issuecomment-2149673942 - # - # let!(:key_provider1) { ActiveRecord::Encryption::DerivedSecretKeyProvider.new(SecureRandom.base64(32)) } - # - # before do - # ActiveRecord::Encryption.with_encryption_context(key_provider: key_provider1) do - # create(:dependency_proxy_group_setting) - # end - # end - # - # Until then, we can only use the default key provider - let!(:key_provider1) { ActiveRecord::Encryption.key_provider } + let(:current_key_provider) { ActiveRecord::Encryption.key_provider } + let(:unknown_key_provider) { ActiveRecord::Encryption::DerivedSecretKeyProvider.new(SecureRandom.base64(32)) } before do + # Create a record with the current encryption key create(:dependency_proxy_group_setting) + + # Create a record with a different encryption key + ActiveRecord::Encryption.with_encryption_context(key_provider: unknown_key_provider) do + create(:dependency_proxy_group_setting) + end end it 'detects decryptable secrets' do - expect(logger).to receive(:info).with(/Encryption keys usage for DependencyProxy::GroupSetting:/) - expect(logger).to receive(:info).with(/- `#{key_provider1.encryption_key.id}` => 2/) + expect(logger).to receive(:info).with("Encryption keys usage for DependencyProxy::GroupSetting:") + expect(logger).to receive(:info).with("- `#{current_key_provider.encryption_key.id}` => 2") + expect(logger).to receive(:info).with("- `#{unknown_key_provider.encryption_key.id}` (UNKNOWN KEY!) => 2") doctor_encryption_secrets end diff --git a/spec/lib/gitlab/slash_commands/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/issue_move_spec.rb index 33f9532b51a..6e09090d1ab 100644 --- a/spec/lib/gitlab/slash_commands/issue_move_spec.rb +++ b/spec/lib/gitlab/slash_commands/issue_move_spec.rb @@ -44,14 +44,9 @@ RSpec.describe Gitlab::SlashCommands::IssueMove, :service, feature_category: :te it 'returns the error message' do message = "issue move #{issue.iid} #{project.full_path}" - if issue.project.work_item_move_and_clone_flag_enabled? - process_message(message) - # move does not happen, as moving issue to same project results in same issue, but we do not show an error - expect(issue.reload.moved_to).to be_nil - else - expect(process_message(message)).to include(response_type: :ephemeral, - text: a_string_matching(same_project_error_message)) - end + process_message(message) + # move does not happen, as moving issue to same project results in same issue, but we do not show an error + expect(issue.reload.moved_to).to be_nil end end @@ -119,30 +114,11 @@ RSpec.describe Gitlab::SlashCommands::IssueMove, :service, feature_category: :te other_project.team.add_guest(user) expect(process_message(message)).to include(response_type: :ephemeral, - text: a_string_matching(permissions_error_message)) + text: a_string_matching("Unable to move. You have insufficient permissions.")) end end end - context 'with work_item_move_and_clone disabled' do - before do - stub_feature_flags(work_item_move_and_clone: false) - end - - it_behaves_like 'move issue slash command' do - let(:same_project_error_message) { "Cannot move issue to project it originates from." } - let(:permissions_error_message) { "Cannot move issue due to insufficient permissions." } - end - end - - context 'with work_item_move_and_clone enabled' do - before do - stub_feature_flags(work_item_move_and_clone: true) - end - - it_behaves_like 'move issue slash command' do - let(:permissions_error_message) { "Unable to move. You have insufficient permissions." } - end - end + it_behaves_like 'move issue slash command' end end diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb index abaa2d4b250..f8239162063 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb @@ -2,14 +2,18 @@ require 'spec_helper' -RSpec.describe Gitlab::SlashCommands::Presenters::IssueMove do +RSpec.describe Gitlab::SlashCommands::Presenters::IssueMove, feature_category: :team_planning do let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, developers: user) } let_it_be(:other_project) { create(:project, developers: user) } let_it_be(:old_issue, reload: true) { create(:issue, project: project) } - let(:new_issue) { Issues::MoveService.new(container: project, current_user: user).execute(old_issue, other_project) } let(:attachment) { subject[:attachments].first } + let(:new_issue) do + ::WorkItems::DataSync::MoveService.new( + work_item: old_issue, current_user: user, target_namespace: other_project.project_namespace + ).execute[:work_item] + end subject { described_class.new(new_issue).present(old_issue) } diff --git a/spec/migrations/20250505095336_queue_mark_admin_bot_runners_as_hosted_spec.rb b/spec/migrations/20250505095336_queue_mark_admin_bot_runners_as_hosted_spec.rb new file mode 100644 index 00000000000..4738d49a36d --- /dev/null +++ b/spec/migrations/20250505095336_queue_mark_admin_bot_runners_as_hosted_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe QueueMarkAdminBotRunnersAsHosted, migration: :gitlab_ci, feature_category: :hosted_runners do + let!(:batched_migration) { described_class::MIGRATION } + + it 'schedules a new batched migration' do + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + gitlab_schema: :gitlab_ci, + table_name: :ci_runners, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index 43d691e1b4a..de6cdc7b147 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -575,12 +575,33 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do end end - describe '#linked_items_keyset_order' do + describe '.linked_items_keyset_order' do subject { described_class.linked_items_keyset_order } it { is_expected.to eq('"issue_links"."id" DESC') } end + describe '.linked_items_for' do + let_it_be(:items) { create_list(:work_item, 3, project: reusable_project) } + let_it_be(:linked_items) { create_list(:work_item, 3, project: reusable_project) } + + let(:work_item_ids) { items.pluck(:id) } + + subject(:linked) { described_class.linked_items_for(work_item_ids) } + + before do + items.each_with_index do |item, i| + create(:work_item_link, source: item, target: linked_items[i]) + end + end + + it 'returns the linked items' do + expect(linked.map(&:issue_link_target_id)).to match_array(work_item_ids) + expect(linked.map(&:issue_link_source_id)).to match_array(linked_items.map(&:id)) + expect(linked.map(&:issue_link_type).uniq).to contain_exactly('relates_to') + end + end + context 'with hierarchy' do let_it_be(:type1) { create(:work_item_type, :non_default) } let_it_be(:type2) { create(:work_item_type, :non_default) } diff --git a/spec/requests/api/graphql/mutations/issues/move_spec.rb b/spec/requests/api/graphql/mutations/issues/move_spec.rb index af9ae0371f9..61ca0e14b0e 100644 --- a/spec/requests/api/graphql/mutations/issues/move_spec.rb +++ b/spec/requests/api/graphql/mutations/issues/move_spec.rb @@ -46,13 +46,11 @@ RSpec.describe 'Moving an issue', feature_category: :team_planning do issue.project.add_developer(user) end - context 'with work_item_move_and_clone disabled' do - it 'returns an error' do - post_graphql_mutation(mutation, current_user: user) + it 'returns an error' do + post_graphql_mutation(mutation, current_user: user) - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['errors'][0]).to eq(permissions_error_message) - end + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors'][0]).to eq("Unable to move. You have insufficient permissions.") end end @@ -73,23 +71,5 @@ RSpec.describe 'Moving an issue', feature_category: :team_planning do end end - context 'with work_item_move_and_clone disabled' do - it_behaves_like 'move work item mutation request' do - let(:permissions_error_message) { "Cannot move issue due to insufficient permissions." } - - before do - stub_feature_flags(work_item_move_and_clone: false) - end - end - end - - context 'with work_item_move_and_clone enabled' do - it_behaves_like 'move work item mutation request' do - let(:permissions_error_message) { "Unable to move. You have insufficient permissions." } - - before do - stub_feature_flags(work_item_move_and_clone: true) - end - end - end + it_behaves_like 'move work item mutation request' end diff --git a/spec/requests/api/graphql/project/work_items_spec.rb b/spec/requests/api/graphql/project/work_items_spec.rb index b5c210e2a9d..fe24485c51b 100644 --- a/spec/requests/api/graphql/project/work_items_spec.rb +++ b/spec/requests/api/graphql/project/work_items_spec.rb @@ -3,12 +3,8 @@ require 'spec_helper' RSpec.describe 'getting a work item list for a project', feature_category: :team_planning do - include GraphqlHelpers + include_context 'with work items list request' - let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, :repository, :public, group: group) } - let_it_be(:current_user) { create(:user) } - let_it_be(:reporter) { create(:user, reporter_of: project) } let_it_be(:label1) { create(:label, project: project) } let_it_be(:label2) { create(:label, project: project) } let_it_be(:milestone1) { create(:milestone, project: project) } @@ -36,14 +32,6 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team let(:items_data) { graphql_data['project']['workItems']['nodes'] } let(:item_filter_params) { {} } - let(:fields) do - <<~QUERY - nodes { - #{all_graphql_fields_for('workItems'.classify, max_depth: 2)} - } - QUERY - end - before_all do # Ensure support bot user is created so creation doesn't count towards query limit # and we don't try to obtain an exclusive lease within a transaction. @@ -503,21 +491,35 @@ RSpec.describe 'getting a work item list for a project', feature_category: :team create(:work_item_link, source: item2, target: related_items[0], link_type: 'relates_to') end - it 'executes limited number of N+1 queries', :use_sql_query_cache do - post_graphql(query, current_user: current_user) # warm-up + shared_examples 'query with limited N+1 queries' do |threshold: 0| + it :use_sql_query_cache do + post_graphql(query, current_user: current_user) # warm-up - control = ActiveRecord::QueryRecorder.new(skip_cached: false) do - post_graphql(query, current_user: current_user) + control = ActiveRecord::QueryRecorder.new(skip_cached: false) do + post_graphql(query, current_user: current_user) + end + + item3 = create(:work_item, project: project, discussion_locked: true, title: 'item1', labels: [label1]) + + [item1, item2, item3].each do |item| + create(:work_item_link, source: item, target: related_items[1], link_type: 'relates_to') + create(:work_item_link, source: item, target: related_items[2], link_type: 'relates_to') + end + + expect_graphql_errors_to_be_empty + expect { post_graphql(query, current_user: current_user) } + .not_to exceed_all_query_limit(control).with_threshold(threshold) + end + end + + it_behaves_like 'query with limited N+1 queries' + + context 'when batch_load_linked_items feature flag is disabled' do + before do + stub_feature_flags(batch_load_linked_items: false) end - [item1, item2].each do |item| - create(:work_item_link, source: item, target: related_items[1], link_type: 'relates_to') - create(:work_item_link, source: item, target: related_items[2], link_type: 'relates_to') - end - - expect_graphql_errors_to_be_empty - expect { post_graphql(query, current_user: current_user) } - .not_to exceed_all_query_limit(control) + it_behaves_like 'query with limited N+1 queries', threshold: 33 end end diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb index 73a620620a7..bad58b388e5 100644 --- a/spec/requests/api/issues/post_projects_issues_spec.rb +++ b/spec/requests/api/issues/post_projects_issues_spec.rb @@ -488,15 +488,9 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin context 'when source and target projects are the same' do it 'returns 400 when trying to move an issue' do - post api(path, user), - params: { to_project_id: project.id } + post api(path, user), params: { to_project_id: project.id } - if issue.project.work_item_move_and_clone_flag_enabled? - expect(json_response['id']).to eq(issue.id) - else - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq(same_project_error_message) - end + expect(json_response['id']).to eq(issue.id) end end @@ -506,7 +500,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin params: { to_project_id: target_project2.id } expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq(permissions_error_message) + expect(json_response['message']).to eq("Unable to move. You have insufficient permissions.") end end @@ -558,26 +552,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin end end - context 'with work_item_move_and_clone disabled' do - it_behaves_like 'move work item api requests' do - let(:same_project_error_message) { "Cannot move issue to project it originates from." } - let(:permissions_error_message) { "Cannot move issue due to insufficient permissions." } - - before do - stub_feature_flags(work_item_move_and_clone: false) - end - end - end - - context 'with work_item_move_and_clone enabled' do - it_behaves_like 'move work item api requests' do - let(:permissions_error_message) { "Unable to move. You have insufficient permissions." } - - before do - stub_feature_flags(work_item_move_and_clone: true) - end - end - end + it_behaves_like 'move work item api requests' end describe '/projects/:id/issues/:issue_iid/clone' do @@ -598,12 +573,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin cloned_issue = Issue.last - # legacy clone adds an extra note for assignees, even though assignees are cloned from original issue and, - # in case of cloning notes, along with cloning assignments system notes as well - notes_count = 2 - notes_count -= 1 if issue.project.work_item_move_and_clone_flag_enabled? - - expect(cloned_issue.notes.count).to eq(notes_count) + expect(cloned_issue.notes.count).to eq(1) expect(cloned_issue.notes.pluck(:note)).not_to include(issue.notes.first.note) expect(response).to have_gitlab_http_status(:created) expect(json_response['id']).to eq(cloned_issue.id) @@ -630,7 +600,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin params: { to_project_id: invalid_target_project.id } expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['message']).to eq(permissions_error_message) + expect(json_response['message']).to eq("Unable to clone. You have insufficient permissions.") end end @@ -682,11 +652,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin cloned_issue = Issue.last - # legacy clone adds an extra note for assignees, even though those we cloned from original issue - notes_count = issue.notes.count + 1 - notes_count -= 1 if issue.project.work_item_move_and_clone_flag_enabled? - - expect(cloned_issue.notes.count).to eq(notes_count) + expect(cloned_issue.notes.count).to eq(issue.notes.count) expect(cloned_issue.notes.pluck(:note)).to include(issue.notes.first.note) expect(response).to have_gitlab_http_status(:created) expect(json_response['id']).to eq(cloned_issue.id) @@ -694,25 +660,7 @@ RSpec.describe API::Issues, :aggregate_failures, feature_category: :team_plannin end end - context 'with work_item_move_and_clone disabled' do - before do - stub_feature_flags(work_item_move_and_clone: false) - end - - it_behaves_like 'clone work item api requests' do - let(:permissions_error_message) { "Cannot clone issue due to insufficient permissions." } - end - end - - context 'with work_item_move_and_clone enabled' do - before do - stub_feature_flags(work_item_move_and_clone: true) - end - - it_behaves_like 'clone work item api requests' do - let(:permissions_error_message) { "Unable to clone. You have insufficient permissions." } - end - end + it_behaves_like 'clone work item api requests' end describe 'POST :id/issues/:issue_iid/subscribe' do diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb index 051b8b5b89d..d265247cdad 100644 --- a/spec/serializers/issue_entity_spec.rb +++ b/spec/serializers/issue_entity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe IssueEntity do +RSpec.describe IssueEntity, feature_category: :team_planning do include Gitlab::Routing.url_helpers let(:project) { create(:project) } @@ -58,7 +58,9 @@ RSpec.describe IssueEntity do before do project.add_developer(member) public_project.add_developer(member) - Issues::MoveService.new(container: public_project, current_user: member).execute(issue, project) + ::WorkItems::DataSync::MoveService.new( + work_item: issue, current_user: member, target_namespace: project.project_namespace + ).execute end context 'when user cannot read target project' do diff --git a/spec/services/design_management/copy_design_collection/queue_service_spec.rb b/spec/services/design_management/copy_design_collection/queue_service_spec.rb deleted file mode 100644 index e6809e65d8a..00000000000 --- a/spec/services/design_management/copy_design_collection/queue_service_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true -require 'spec_helper' - -RSpec.describe DesignManagement::CopyDesignCollection::QueueService, :clean_gitlab_redis_shared_state, - feature_category: :design_management do - include DesignManagementTestHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:issue) { create(:issue) } - let_it_be(:target_issue, refind: true) { create(:issue) } - let_it_be(:design) { create(:design, issue: issue, project: issue.project) } - - subject { described_class.new(user, issue, target_issue).execute } - - before do - enable_design_management - end - - it 'returns an error if user does not have permission' do - expect(subject).to be_kind_of(ServiceResponse) - expect(subject).to be_error - expect(subject.message).to eq('User cannot copy designs to issue') - end - - context 'when user has permission' do - before_all do - issue.project.add_reporter(user) - target_issue.project.add_reporter(user) - end - - it 'returns an error if design collection copy_state is not queuable' do - target_issue.design_collection.start_copy! - - expect(subject).to be_kind_of(ServiceResponse) - expect(subject).to be_error - expect(subject.message).to eq('Target design collection copy state must be `ready`') - end - - it 'sets the design collection copy state' do - expect { subject }.to change { target_issue.design_collection.copy_state }.from('ready').to('in_progress') - end - - it 'queues a DesignManagement::CopyDesignCollectionWorker', :clean_gitlab_redis_queues do - expect { subject }.to change(DesignManagement::CopyDesignCollectionWorker.jobs, :size).by(1) - end - - it 'returns success' do - expect(subject).to be_kind_of(ServiceResponse) - expect(subject).to be_success - end - end -end diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb deleted file mode 100644 index a1652b3fa85..00000000000 --- a/spec/services/issues/clone_service_spec.rb +++ /dev/null @@ -1,445 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Issues::CloneService, feature_category: :team_planning do - include DesignManagementTestHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:author) { create(:user) } - let_it_be(:title) { 'Some issue' } - let_it_be(:description) { "Some issue description with mention to #{user.to_reference}" } - let_it_be(:group) { create(:group, :private) } - let_it_be(:sub_group_1) { create(:group, :private, parent: group) } - let_it_be(:sub_group_2) { create(:group, :private, parent: group) } - let_it_be(:old_project) { create(:project, namespace: sub_group_1) } - let_it_be(:new_project) { create(:project, namespace: sub_group_2) } - - let_it_be(:old_issue, reload: true) do - create(:issue, title: title, description: description, project: old_project, author: author, imported_from: :gitlab_migration) - end - - let(:with_notes) { false } - - subject(:clone_service) do - described_class.new(container: old_project, current_user: user) - end - - shared_context 'user can clone issue' do - before do - old_project.add_reporter(user) - new_project.add_reporter(user) - end - end - - describe '#execute' do - context 'issue movable' do - include_context 'user can clone issue' - - context 'when issue creation fails' do - before do - allow_next_instance_of(Issues::CreateService) do |create_service| - allow(create_service).to receive(:execute).and_return(ServiceResponse.error(message: 'some error')) - end - end - - it 'raises a clone error' do - expect { clone_service.execute(old_issue, new_project) }.to raise_error( - Issues::CloneService::CloneError, - 'some error' - ) - end - end - - # We will use this service in order to clone WorkItem to a new project. As WorkItem inherits from Issue, there - # should not be any problem with passing a WorkItem instead of an Issue to this service. - # Adding a small test case to cover this. - context "when we pass a work_item" do - include_context 'user can clone issue' - - subject(:clone) { clone_service.execute(original_work_item, new_project) } - - context "work item is of issue type" do - let_it_be_with_reload(:original_work_item) { create(:work_item, :issue, project: old_project, author: author) } - - it { expect { clone }.to change { new_project.issues.count }.by(1) } - end - - context "work item is of task type" do - let_it_be_with_reload(:original_work_item) { create(:work_item, :task, project: old_project, author: author) } - - it { expect { clone }.to raise_error(described_class::CloneError) } - end - end - - context 'generic issue' do - let!(:new_issue) { clone_service.execute(old_issue, new_project, with_notes: with_notes) } - - it 'creates a new issue in the selected project' do - expect do - clone_service.execute(old_issue, new_project) - end.to change { new_project.issues.count }.by(1) - end - - it 'copies issue title' do - expect(new_issue.title).to eq title - end - - it 'copies issue description' do - expect(new_issue.description).to eq description - end - - it 'restores imported_from to none' do - expect(old_issue.reload.imported_from).to eq 'gitlab_migration' - expect(new_issue.imported_from).to eq 'none' - end - - it 'adds system note to old issue at the end' do - expect(old_issue.notes.last.note).to start_with 'cloned to' - end - - it 'adds system note to new issue at the start' do - # We set an assignee so an assignee system note will be generated and - # we can assert that the "cloned from" note is the first one - assignee = create(:user) - new_project.add_developer(assignee) - old_issue.assignees = [assignee] - - new_issue = clone_service.execute(old_issue, new_project) - - expect(new_issue.notes.size).to eq(2) - - cloned_from_note = new_issue.notes.last - expect(cloned_from_note.note).to start_with 'cloned from' - expect(new_issue.notes.fresh.first).to eq(cloned_from_note) - end - - it 'keeps old issue open' do - expect(old_issue.open?).to be true - end - - it 'persists new issue' do - expect(new_issue.persisted?).to be true - end - - it 'persists all changes' do - expect(old_issue.changed?).to be false - expect(new_issue.changed?).to be false - end - - it 'sets the current user as author' do - expect(new_issue.author).to eq user - end - - it 'creates a new internal id for issue' do - expect(new_issue.iid).to be_present - end - - it 'sets created_at of new issue to the time of clone' do - future_time = 5.days.from_now - - travel_to(future_time) do - new_issue = clone_service.execute(old_issue, new_project, with_notes: with_notes) - - expect(new_issue.created_at).to be_like_time(future_time) - end - end - - it 'does not set moved_issue' do - expect(old_issue.moved?).to eq(false) - end - - context 'when copying comments' do - let(:with_notes) { true } - - it 'does not create extra system notes' do - new_issue = clone_service.execute(old_issue, new_project, with_notes: with_notes) - - expect(new_issue.notes.count).to eq(old_issue.notes.count) - end - end - end - - context 'sends notifications to participants' do - it 'notifies participants' do - expect_next_instance_of(NotificationService) do |notification| - expect(notification).to receive_message_chain(:async, :issue_cloned) - end - - clone_service.execute(old_issue, new_project) - end - end - - context 'issue with system notes and resource events' do - before do - create(:note, :system, noteable: old_issue, project: old_project) - create(:resource_label_event, label: create(:label, project: old_project), issue: old_issue) - create(:resource_state_event, issue: old_issue, state: :reopened) - create(:resource_milestone_event, issue: old_issue, action: 'remove', milestone_id: nil) - end - - it 'does not copy system notes and resource events' do - new_issue = clone_service.execute(old_issue, new_project) - - # 1 here is for the "cloned from" system note - expect(new_issue.notes.count).to eq(1) - expect(new_issue.resource_state_events).to be_empty - expect(new_issue.resource_milestone_events).to be_empty - end - end - - context 'issue with award emoji' do - let!(:award_emoji) { create(:award_emoji, awardable: old_issue) } - - it 'does not copy the award emoji' do - old_issue.reload - new_issue = clone_service.execute(old_issue, new_project) - - expect(new_issue.reload.award_emoji).to be_empty - end - end - - context 'issue with milestone' do - let(:milestone) { create(:milestone, group: sub_group_1) } - let(:new_project) { create(:project, namespace: sub_group_1) } - - let(:old_issue) do - create(:issue, title: title, description: description, project: old_project, author: author, milestone: milestone) - end - - it 'copies the milestone and creates a resource_milestone_event' do - new_issue = clone_service.execute(old_issue, new_project) - - expect(new_issue.milestone).to eq(milestone) - expect(new_issue.resource_milestone_events.count).to eq(1) - end - end - - context 'issue with label' do - let(:label) { create(:group_label, group: sub_group_1) } - let(:new_project) { create(:project, namespace: sub_group_1) } - - let(:old_issue) do - create(:issue, project: old_project, labels: [label]) - end - - it 'copies the label and creates a resource_label_event' do - new_issue = clone_service.execute(old_issue, new_project) - - expect(new_issue.labels).to contain_exactly(label) - expect(new_issue.resource_label_events.count).to eq(1) - end - end - - context 'issue with due date' do - let(:date) { Date.parse('2020-01-10') } - let(:new_date) { date + 1.week } - - let(:old_issue) do - create(:issue, title: title, description: description, project: old_project, author: author, due_date: date) - end - - before do - old_issue.update!(due_date: new_date) - SystemNoteService.change_start_date_or_due_date(old_issue, old_project, author, old_issue.previous_changes.slice('due_date')) - end - - it 'keeps the same due date' do - new_issue = clone_service.execute(old_issue, new_project) - - expect(new_issue.due_date).to eq(old_issue.due_date) - end - end - - context 'issue with assignee' do - let_it_be(:assignee) { create(:user) } - - before do - old_issue.assignees = [assignee] - end - - it 'preserves assignee with access to the new issue' do - new_project.add_reporter(assignee) - - new_issue = clone_service.execute(old_issue, new_project) - - expect(new_issue.assignees).to eq([assignee]) - end - - it 'ignores assignee without access to the new issue' do - new_issue = clone_service.execute(old_issue, new_project) - - expect(new_issue.assignees).to be_empty - end - end - - context 'issue is confidential' do - before do - old_issue.update_columns(confidential: true) - end - - it 'preserves the confidential flag' do - new_issue = clone_service.execute(old_issue, new_project) - - expect(new_issue.confidential).to be true - end - end - - context 'moving to same project' do - it 'also works' do - new_issue = clone_service.execute(old_issue, old_project) - - expect(new_issue.project).to eq(old_project) - expect(new_issue.iid).not_to eq(old_issue.iid) - end - end - - context 'project issue hooks' do - let!(:hook) { create(:project_hook, project: old_project, issues_events: true) } - - it 'executes project issue hooks' do - allow_next_instance_of(WebHookService) do |instance| - allow(instance).to receive(:execute) - end - - # Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1, - # but since the entire spec run takes place in a transaction, we never - # actually get to the `after_commit` hook that queues these jobs. - expect { clone_service.execute(old_issue, new_project) } - .not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError - end - end - - # These tests verify that notes are copied. More thorough tests are in - # the unit test for Notes::CopyService. - context 'issue with notes' do - let_it_be(:notes) do - [ - create(:note, noteable: old_issue, project: old_project, created_at: 2.weeks.ago, updated_at: 1.week.ago), - create(:note, noteable: old_issue, project: old_project) - ] - end - - let(:new_issue) { clone_service.execute(old_issue, new_project, with_notes: with_notes) } - - let(:copied_notes) { new_issue.notes.limit(notes.size) } # Remove the system note added by the copy itself - - it 'does not copy notes' do - # only the system note - expect(copied_notes.order('id ASC').pluck(:note).size).to eq(1) - end - - context 'when copying comments' do - let(:with_notes) { true } - - it 'copies existing notes in order' do - expect(copied_notes.order('id ASC').pluck(:note)).to eq(notes.map(&:note)) - end - end - end - - context 'issue with a design', :clean_gitlab_redis_shared_state do - let_it_be(:new_project) { create(:project) } - - let!(:design) { create(:design, :with_lfs_file, issue: old_issue) } - let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) } - let(:subject) { clone_service.execute(old_issue, new_project) } - - before do - enable_design_management - end - - it 'calls CopyDesignCollection::QueueService' do - expect(DesignManagement::CopyDesignCollection::QueueService).to receive(:new) - .with(user, old_issue, kind_of(Issue)) - .and_call_original - - subject - end - - it 'logs if QueueService returns an error', :aggregate_failures do - error_message = 'error' - - expect_next_instance_of(DesignManagement::CopyDesignCollection::QueueService) do |service| - expect(service).to receive(:execute).and_return( - ServiceResponse.error(message: error_message) - ) - end - expect(Gitlab::AppLogger).to receive(:error).with(error_message) - - subject - end - - # Perform a small integration test to ensure the services and worker - # can correctly create designs. - it 'copies the design and its notes', :sidekiq_inline, :aggregate_failures do - new_issue = subject - - expect(new_issue.designs.size).to eq(1) - expect(new_issue.designs.first.notes.size).to eq(1) - end - end - - context 'issue relative position' do - let(:subject) { clone_service.execute(old_issue, new_project) } - - it_behaves_like 'copy or reset relative position' - end - end - - describe 'clone permissions' do - let(:clone) { clone_service.execute(old_issue, new_project) } - - context 'target project is pending deletion' do - include_context 'user can clone issue' - - before do - new_project.update_columns(pending_delete: true) - end - - after do - new_project.update_columns(pending_delete: false) - end - - it { expect { clone }.to raise_error(Issues::CloneService::CloneError, /pending deletion/) } - end - - context 'user is reporter in both projects' do - include_context 'user can clone issue' - it { expect { clone }.not_to raise_error } - end - - context 'user is reporter only in new project' do - before do - new_project.add_reporter(user) - end - - it { expect { clone }.to raise_error(StandardError, /permissions/) } - end - - context 'user is reporter only in old project' do - before do - old_project.add_reporter(user) - end - - it { expect { clone }.to raise_error(StandardError, /permissions/) } - end - - context 'user is reporter in one project and guest in another' do - before do - new_project.add_guest(user) - old_project.add_reporter(user) - end - - it { expect { clone }.to raise_error(StandardError, /permissions/) } - end - - context 'issue is not persisted' do - include_context 'user can clone issue' - let(:old_issue) { build(:issue, project: old_project, author: author) } - - it { expect { clone }.to raise_error(StandardError, /permissions/) } - end - end - end -end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb deleted file mode 100644 index 149e4b506a5..00000000000 --- a/spec/services/issues/move_service_spec.rb +++ /dev/null @@ -1,685 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Issues::MoveService, feature_category: :team_planning do - include DesignManagementTestHelpers - - let_it_be(:user) { create(:user) } - let_it_be(:author) { create(:user) } - let_it_be(:title) { 'Some issue' } - let_it_be(:description) { "Some issue description with mention to #{user.to_reference}" } - let_it_be(:group) { create(:group, :private) } - let_it_be(:sub_group_1) { create(:group, :private, parent: group) } - let_it_be(:sub_group_2) { create(:group, :private, parent: group) } - let_it_be(:old_project) { create(:project, namespace: sub_group_1) } - let_it_be(:new_project) { create(:project, namespace: sub_group_2) } - - let_it_be_with_reload(:old_issue) do - create( - :issue, - title: title, - description: description, - project: old_project, - author: author, - imported_from: :gitlab_migration, - created_at: 1.day.ago, - updated_at: 1.day.ago - ) - end - - subject(:move_service) do - described_class.new(container: old_project, current_user: user) - end - - before_all do - # Ensure support bot user is created so creation doesn't count towards query limit - # and we don't try to obtain an exclusive lease within a transaction. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/509629 - Users::Internal.support_bot_id - end - - shared_context 'user can move issue' do - before do - old_project.add_reporter(user) - new_project.add_reporter(user) - end - end - - describe '#execute' do - shared_context 'issue move executed' do - let!(:new_issue) { move_service.execute(old_issue, new_project) } - end - - # We will use this service in order to move WorkItem to a new project. As WorkItem inherits from Issue, there - # should not be any problem with passing a WorkItem instead of an Issue to this service. - # Adding a small test case to cover this. - context "when we pass a work_item" do - include_context 'user can move issue' - - subject(:move) { move_service.execute(original_work_item, new_project) } - - context "work item is of issue type" do - let_it_be_with_reload(:original_work_item) { create(:work_item, :issue, project: old_project, author: author) } - - it { expect { move }.to change { new_project.issues.count }.by(1) } - end - - context "work item is of task type" do - let_it_be_with_reload(:original_work_item) { create(:work_item, :task, project: old_project, author: author) } - - it { expect { move }.to raise_error(described_class::MoveError) } - end - end - - context 'when issue creation fails' do - include_context 'user can move issue' - - before do - allow_next_instance_of(Issues::CreateService) do |create_service| - allow(create_service).to receive(:execute).and_return(ServiceResponse.error(message: 'some error')) - end - end - - it 'raises a move error' do - expect { move_service.execute(old_issue, new_project) }.to raise_error( - Issues::MoveService::MoveError, - 'some error' - ) - end - end - - context 'issue movable' do - include_context 'user can move issue' - - it 'creates resource state event' do - expect { move_service.execute(old_issue, new_project) }.to change(ResourceStateEvent.where(issue_id: old_issue), :count).by(1) - end - - context 'generic issue' do - include_context 'issue move executed' - - it 'creates a new issue in a new project' do - expect(new_issue.project).to eq new_project - expect(new_issue.namespace_id).to eq new_project.project_namespace_id - end - - it 'copies issue title' do - expect(new_issue.title).to eq title - end - - it 'copies issue description' do - expect(new_issue.description).to eq description - end - - it 'restores imported_from to none' do - expect(new_issue.imported_from).to eq 'none' - expect(old_issue.reload.imported_from).to eq 'gitlab_migration' - end - - it 'adds system note to old issue at the end' do - expect(old_issue.notes.last.note).to start_with 'moved to' - end - - it 'adds system note to new issue at the end', :freeze_time do - system_note = new_issue.notes.last - - expect(system_note.note).to start_with 'moved from' - expect(system_note.created_at).to be_like_time(Time.current) - end - - it 'closes old issue' do - expect(old_issue.closed?).to be true - end - - it 'persists new issue' do - expect(new_issue.persisted?).to be true - end - - it 'persists all changes' do - expect(old_issue.changed?).to be false - expect(new_issue.changed?).to be false - end - - it 'preserves author' do - expect(new_issue.author).to eq author - end - - it 'creates a new internal id for issue' do - expect(new_issue.iid).to be 1 - end - - it 'marks issue as moved' do - expect(old_issue.moved?).to eq true - expect(old_issue.moved_to).to eq new_issue - end - - it 'marks issue as closed' do - expect(old_issue.closed?).to eq true - end - - it 'preserves create time' do - expect(old_issue.created_at).to eq new_issue.created_at - end - end - - context 'sends notifications to participants' do - it 'notifies participants' do - expect_next_instance_of(NotificationService) do |notification| - expect(notification).to receive_message_chain(:async, :issue_moved) - end - - move_service.execute(old_issue, new_project) - end - end - - context 'issue with children' do - let_it_be(:task1) { create(:issue, :task, project: old_issue.project) } - let_it_be(:task2) { create(:issue, :task, project: old_issue.project) } - - before_all do - create(:parent_link, work_item_parent_id: old_issue.id, work_item_id: task1.id) - create(:parent_link, work_item_parent_id: old_issue.id, work_item_id: task2.id) - end - - it "moves the issue and each of it's children", :aggregate_failures do - expect { move_service.execute(old_issue, new_project) }.to change { Issue.count }.by(3) - expect(new_project.issues.count).to eq(3) - expect(new_project.issues.pluck(:title)).to match_array( - [old_issue, task1, task2].map(&:title) - ) - end - end - - context 'issue with award emoji' do - let!(:award_emoji) { create(:award_emoji, awardable: old_issue) } - - it 'copies the award emoji' do - old_issue.reload - new_issue = move_service.execute(old_issue, new_project) - - expect(old_issue.award_emoji.first.name).to eq new_issue.reload.award_emoji.first.name - end - end - - context 'issue with milestone' do - let(:milestone) { create(:milestone, group: sub_group_1) } - let(:new_project) { create(:project, namespace: sub_group_1) } - - let(:old_issue) do - create(:issue, title: title, description: description, project: old_project, author: author, milestone: milestone) - end - - before do - create(:resource_milestone_event, issue: old_issue, milestone: milestone, action: :add) - end - - it 'does not create extra milestone events' do - new_issue = move_service.execute(old_issue, new_project) - - expect(new_issue.resource_milestone_events.count).to eq(old_issue.resource_milestone_events.count) - end - end - - context 'issue with due date' do - let(:old_issue) do - create(:issue, title: title, description: description, project: old_project, author: author, due_date: '2020-01-10') - end - - before do - old_issue.update!(due_date: Date.today) - SystemNoteService.change_start_date_or_due_date(old_issue, old_project, author, old_issue.previous_changes.slice('due_date')) - end - - it 'does not create extra system notes' do - new_issue = move_service.execute(old_issue, new_project) - - expect(new_issue.notes.count).to eq(old_issue.notes.count) - end - end - - context 'issue with assignee' do - let_it_be(:assignee) { create(:user) } - - before do - old_issue.assignees = [assignee] - end - - it 'preserves assignee with access to the new issue' do - new_project.add_reporter(assignee) - - new_issue = move_service.execute(old_issue, new_project) - - expect(new_issue.assignees).to eq([assignee]) - end - - it 'ignores assignee without access to the new issue' do - new_issue = move_service.execute(old_issue, new_project) - - expect(new_issue.assignees).to be_empty - end - end - - context 'issue with contacts' do - let_it_be(:contacts) { create_list(:contact, 2, group: group) } - - before do - old_issue.customer_relations_contacts = contacts - end - - it 'preserves contacts' do - new_issue = move_service.execute(old_issue, new_project) - - expect(new_issue.customer_relations_contacts).to eq(contacts) - end - - context 'when moving to another root group' do - let(:another_project) { create(:project, namespace: create(:group)) } - - before do - another_project.add_reporter(user) - end - - it 'does not preserve contacts' do - new_issue = move_service.execute(old_issue, another_project) - - expect(new_issue.customer_relations_contacts).to be_empty - end - end - end - - context 'moving to same project' do - let(:new_project) { old_project } - - it 'raises error' do - expect { move_service.execute(old_issue, new_project) } - .to raise_error(StandardError, /Cannot move issue/) - end - end - - context 'project issue hooks' do - let_it_be(:old_project_hook) { create(:project_hook, project: old_project, issues_events: true) } - let_it_be(:new_project_hook) { create(:project_hook, project: new_project, issues_events: true) } - - let(:expected_new_project_hook_payload) do - hash_including( - event_type: 'issue', - object_kind: 'issue', - object_attributes: include( - project_id: new_project.id, - state: 'opened', - action: 'open' - ) - ) - end - - let(:expected_old_project_hook_payload) do - hash_including( - event_type: 'issue', - object_kind: 'issue', - changes: { - state_id: { current: 2, previous: 1 }, - closed_at: { current: kind_of(Time), previous: nil }, - updated_at: { current: kind_of(Time), previous: kind_of(Time) } - }, - object_attributes: include( - id: old_issue.id, - closed_at: kind_of(Time), - state: 'closed', - action: 'close' - ) - ) - end - - it 'executes project issue hooks for both projects' do - expect_next_instance_of( - WebHookService, - new_project_hook, - expected_new_project_hook_payload, - 'issue_hooks', - idempotency_key: anything - ) do |service| - expect(service).to receive(:async_execute).once - end - - expect_next_instance_of( - WebHookService, - old_project_hook, - expected_old_project_hook_payload, - 'issue_hooks', - idempotency_key: anything - ) do |service| - expect(service).to receive(:async_execute).once - end - - move_service.execute(old_issue, new_project) - end - end - - # These tests verify that notes are copied. More thorough tests are in - # the unit test for Notes::CopyService. - context 'issue with notes' do - let!(:notes) do - [ - create(:note, noteable: old_issue, project: old_project, created_at: 2.weeks.ago, updated_at: 1.week.ago, imported_from: :gitlab_migration), - create(:note, noteable: old_issue, project: old_project, imported_from: :gitlab_migration) - ] - end - - let(:copied_notes) { new_issue.notes.limit(notes.size) } # Remove the system note added by the copy itself - - include_context 'issue move executed' - - it 'copies existing notes in order' do - expect(copied_notes.order('id ASC').pluck(:note)).to eq(notes.map(&:note)) - end - - it 'resets the imported_from value to none' do - expect(notes.map(&:reload)).to all(have_attributes(imported_from: 'gitlab_migration')) - - expect(copied_notes.pluck(:imported_from)).to all(eq('none')) - end - end - - context 'issue with a design', :clean_gitlab_redis_shared_state do - let_it_be(:new_project) { create(:project) } - - let!(:design) { create(:design, :with_lfs_file, issue: old_issue) } - let!(:note) { create(:diff_note_on_design, noteable: design, issue: old_issue, project: old_issue.project) } - let(:subject) { move_service.execute(old_issue, new_project) } - - before do - enable_design_management - end - - it 'calls CopyDesignCollection::QueueService' do - expect(DesignManagement::CopyDesignCollection::QueueService).to receive(:new) - .with(user, old_issue, kind_of(Issue)) - .and_call_original - - subject - end - - it 'logs if QueueService returns an error', :aggregate_failures do - error_message = 'error' - - expect_next_instance_of(DesignManagement::CopyDesignCollection::QueueService) do |service| - expect(service).to receive(:execute).and_return( - ServiceResponse.error(message: error_message) - ) - end - expect(Gitlab::AppLogger).to receive(:error).with(error_message) - - subject - end - - # Perform a small integration test to ensure the services and worker - # can correctly create designs. - it 'copies the design and its notes', :sidekiq_inline, :aggregate_failures do - new_issue = subject - - expect(new_issue.designs.size).to eq(1) - expect(new_issue.designs.first.notes.size).to eq(1) - end - end - - context 'issue with timelogs' do - before do - create(:timelog, issue: old_issue) - end - - it 'calls CopyTimelogsWorker' do - expect(WorkItems::CopyTimelogsWorker).to receive(:perform_async).with(old_issue.id, kind_of(Integer)) - - move_service.execute(old_issue, new_project) - end - end - - context 'issue relative position' do - let(:subject) { move_service.execute(old_issue, new_project) } - - it_behaves_like 'copy or reset relative position' - end - - context 'issue with escalation status' do - it 'keeps the escalation status' do - escalation_status = create(:incident_management_issuable_escalation_status, issue: old_issue) - - move_service.execute(old_issue, new_project) - - expect(escalation_status.reload.issue).to eq(old_issue) - end - end - end - - describe 'move permissions' do - let(:move) { move_service.execute(old_issue, new_project) } - - context 'user is reporter in both projects' do - include_context 'user can move issue' - it { expect { move }.not_to raise_error } - end - - context 'user is reporter only in new project' do - before do - new_project.add_reporter(user) - end - - it { expect { move }.to raise_error(StandardError, /permissions/) } - end - - context 'user is reporter only in old project' do - before do - old_project.add_reporter(user) - end - - it { expect { move }.to raise_error(StandardError, /permissions/) } - end - - context 'user is reporter in one project and guest in another' do - before do - new_project.add_guest(user) - old_project.add_reporter(user) - end - - it { expect { move }.to raise_error(StandardError, /permissions/) } - end - - context 'issue has already been moved' do - include_context 'user can move issue' - - let(:moved_to_issue) { create(:issue) } - - let(:old_issue) do - create(:issue, project: old_project, author: author, moved_to: moved_to_issue) - end - - it { expect { move }.to raise_error(StandardError, /permissions/) } - end - - context 'issue is not persisted' do - include_context 'user can move issue' - let(:old_issue) { build(:issue, project: old_project, author: author) } - - it { expect { move }.to raise_error(StandardError, /permissions/) } - end - end - end - - describe '#recreate_related_issues' do - include_context 'user can move issue' - - let(:admin) { create(:admin) } - let(:authorized_project) { create(:project) } - let(:authorized_project2) { create(:project) } - let(:unauthorized_project) { create(:project) } - - let(:authorized_issue_b) { create(:issue, project: authorized_project) } - let(:authorized_issue_c) { create(:issue, project: authorized_project2) } - let(:authorized_issue_d) { create(:issue, project: authorized_project2) } - let(:unauthorized_issue) { create(:issue, project: unauthorized_project) } - - let!(:issue_link_a) { create(:issue_link, source: old_issue, target: authorized_issue_b) } - let!(:issue_link_b) { create(:issue_link, source: old_issue, target: unauthorized_issue) } - let!(:issue_link_c) { create(:issue_link, source: old_issue, target: authorized_issue_c) } - let!(:issue_link_d) { create(:issue_link, source: authorized_issue_d, target: old_issue) } - - before do - authorized_project.add_developer(user) - authorized_project.add_developer(admin) - authorized_project2.add_developer(user) - authorized_project2.add_developer(admin) - end - - context 'multiple related issues' do - context 'when admin mode is enabled', :enable_admin_mode do - it 'recreates all related issues and retains permissions' do - new_issue = move_service.execute(old_issue, new_project) - - recreated_links = IssueLink.where(source: new_issue).or(IssueLink.where(target: new_issue)) - old_links_count = IssueLink.where(source: old_issue).or(IssueLink.where(target: old_issue)).count - - expect(recreated_links.count).to eq(4) - expect(old_links_count).to eq(0) - - recreated_links.each do |link| - expect(link.namespace_id).to eq(link.source.namespace_id) - end - - expect(new_issue.related_issues(admin)) - .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d, unauthorized_issue]) - - expect(new_issue.related_issues(user)) - .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d]) - - expect(authorized_issue_d.related_issues(user)) - .to match_array([new_issue]) - end - - it 'does not recreate related issues when there are no related links' do - IssueLink.where(source: old_issue).or(IssueLink.where(target: old_issue)).delete_all - - new_issue = move_service.execute(old_issue, new_project) - recreated_links = IssueLink.where(source: new_issue).or(IssueLink.where(target: new_issue)) - - expect(recreated_links.count).to eq(0) - end - end - - context 'when admin mode is disabled' do - it 'recreates authorized related issues only and retains permissions' do - new_issue = move_service.execute(old_issue, new_project) - - recreated_links = IssueLink.where(source: new_issue).or(IssueLink.where(target: new_issue)) - old_links_count = IssueLink.where(source: old_issue).or(IssueLink.where(target: old_issue)).count - - expect(recreated_links.count).to eq(4) - expect(old_links_count).to eq(0) - - recreated_links.each do |link| - expect(link.namespace_id).to eq(link.source.namespace_id) - end - - expect(new_issue.related_issues(admin)) - .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d]) - - expect(new_issue.related_issues(user)) - .to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d]) - - expect(authorized_issue_d.related_issues(user)) - .to match_array([new_issue]) - end - end - end - end - - context 'updating sent notifications' do - let!(:old_issue_notification_1) { create(:sent_notification, project: old_issue.project, noteable: old_issue) } - let!(:old_issue_notification_2) { create(:sent_notification, project: old_issue.project, noteable: old_issue) } - let!(:other_issue_notification) { create(:sent_notification, project: old_issue.project) } - - include_context 'user can move issue' - - context 'when issue is from service desk', :aggregate_failures do - before do - allow(old_issue).to receive(:from_service_desk?).and_return(true) - end - - it 'updates moved issue sent notifications' do - new_issue = move_service.execute(old_issue, new_project) - - old_issue_notification_1.reload - old_issue_notification_2.reload - expect(old_issue_notification_1.project_id).to eq(new_issue.project_id) - expect(old_issue_notification_1.noteable_id).to eq(new_issue.id) - expect(old_issue_notification_2.project_id).to eq(new_issue.project_id) - expect(old_issue_notification_2.noteable_id).to eq(new_issue.id) - end - - it 'does not update other issues sent notifications' do - expect do - move_service.execute(old_issue, new_project) - other_issue_notification.reload - end.not_to change { other_issue_notification.noteable_id } - end - - context 'when the issue has children' do - let(:task) { create(:issue, :task, project: old_issue.project, author: Users::Internal.support_bot) } - let!(:task_notification) { create(:sent_notification, project: old_issue.project, noteable: task) } - - before do - create(:parent_link, work_item_parent_id: old_issue.id, work_item_id: task.id) - end - - it 'updates moved issue sent notifications' do - new_issue = move_service.execute(old_issue, new_project) - new_task = Issue.where(work_item_type: WorkItems::Type.default_by_type(:task)).last - - old_issue_notification_1.reload - old_issue_notification_2.reload - task_notification.reload - expect(old_issue_notification_1.project_id).to eq(new_issue.project_id) - expect(old_issue_notification_1.noteable_id).to eq(new_issue.id) - expect(old_issue_notification_2.project_id).to eq(new_issue.project_id) - expect(old_issue_notification_2.noteable_id).to eq(new_issue.id) - expect(task_notification.project_id).to eq(new_issue.project_id) - expect(task_notification.noteable_id).to eq(new_task.id) - end - end - end - - context 'when issue is not from service desk' do - it 'does not update sent notifications' do - move_service.execute(old_issue, new_project) - - old_issue_notification_1.reload - old_issue_notification_2.reload - expect(old_issue_notification_1.project_id).to eq(old_issue.project_id) - expect(old_issue_notification_1.noteable_id).to eq(old_issue.id) - expect(old_issue_notification_2.project_id).to eq(old_issue.project_id) - expect(old_issue_notification_2.noteable_id).to eq(old_issue.id) - end - end - end - - context 'copying email participants' do - let!(:participant1) { create(:issue_email_participant, email: 'user1@example.com', issue: old_issue) } - let!(:participant2) { create(:issue_email_participant, email: 'user2@example.com', issue: old_issue) } - let!(:participant3) { create(:issue_email_participant, email: 'other_project_customer@example.com') } - - include_context 'user can move issue' - - subject(:new_issue) do - move_service.execute(old_issue, new_project) - end - - it 'copies moved issue email participants' do - new_issue - - expect(participant1.reload.issue).to eq(old_issue) - expect(participant2.reload.issue).to eq(old_issue) - expect(new_issue.issue_email_participants.pluck(:email)) - .to match_array([participant1.email, participant2.email]) - end - end -end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 2df24435b6c..1c29eedb535 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1438,7 +1438,7 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning end it 'calls the move service with the proper issue and project' do - expect_next_instance_of(move_service_class) do |service| + expect_next_instance_of(::WorkItems::DataSync::MoveService) do |service| expect(service).to receive(:execute).and_call_original end @@ -1450,40 +1450,7 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning end end - context 'with work_item_move_and_clone disabled' do - let(:move_service_class) { Issues::MoveService } - let_it_be(:target_project) { create(:project) } - - before do - stub_feature_flags(work_item_move_and_clone: false) - target_project.add_maintainer(user) - end - - it_behaves_like 'move issue to another project' - - context 'when target_clone_container is a ProjectNamespace' do - it 'calls the legacy move service with the proper issue and project' do - expect_next_instance_of(move_service_class) do |service| - expect(service).to receive(:execute).and_call_original - end - - new_issue = update_issue(target_container: target_project.project_namespace) - - expect(new_issue.project).to eq(target_project) - expect(new_issue.title).to eq(issue.title) - end - end - end - - context 'with work_item_move_and_clone enabled' do - it_behaves_like 'move issue to another project' do - let(:move_service_class) { ::WorkItems::DataSync::MoveService } - - before do - stub_feature_flags(work_item_move_and_clone: true) - end - end - end + it_behaves_like 'move issue to another project' context 'when target container is a group' do context 'without access to the group' do @@ -1491,7 +1458,6 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning it 'does not call any clone service' do expect(WorkItems::DataSync::MoveService).not_to receive(:new) - expect(Issues::MoveService).not_to receive(:new) update_issue(target_container: target_container) end @@ -1509,7 +1475,7 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning end it 'calls the move service with the proper issue and project' do - expect_next_instance_of(clone_service_class) do |service| + expect_next_instance_of(::WorkItems::DataSync::CloneService) do |service| expect(service).to receive(:execute).and_call_original end @@ -1521,7 +1487,7 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning context 'clone an issue with notes' do it 'calls the move service with the proper issue and project' do - expect_next_instance_of(clone_service_class) do |service| + expect_next_instance_of(::WorkItems::DataSync::CloneService) do |service| expect(service).to receive(:execute).and_call_original end @@ -1535,38 +1501,7 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning end end - context 'with work_item_move_and_clone disabled' do - let(:clone_service_class) { Issues::CloneService } - - before do - stub_feature_flags(work_item_move_and_clone: false) - end - - it_behaves_like 'clone an issue' - - context 'when target_clone_container is a ProjectNamespace' do - it 'calls the legacy clone service with the proper issue and project' do - expect_next_instance_of(clone_service_class) do |service| - expect(service).to receive(:execute).and_call_original - end - - new_issue = update_issue(target_clone_container: project.project_namespace) - - expect(new_issue.project).to eq(project) - expect(new_issue.title).to eq(issue.title) - end - end - end - - context 'with work_item_move_and_clone enabled' do - before do - stub_feature_flags(work_item_move_and_clone: true) - end - - it_behaves_like 'clone an issue' do - let(:clone_service_class) { ::WorkItems::DataSync::CloneService } - end - end + it_behaves_like 'clone an issue' context 'when target container is a group' do context 'without access to the group' do @@ -1574,28 +1509,10 @@ RSpec.describe Issues::UpdateService, :mailer, feature_category: :team_planning it 'does not call any clone service' do expect(WorkItems::DataSync::CloneService).not_to receive(:new) - expect(Issues::CloneService).not_to receive(:new) update_issue(target_clone_container: target_container, clone_with_notes: true) end end - - context 'when user has access to the group' do - let_it_be(:target_container) { group } - - context 'with work_item_move_and_clone disabled' do - before do - stub_feature_flags(work_item_move_and_clone: false) - end - - it 'does not call any clone service' do - expect(WorkItems::DataSync::CloneService).not_to receive(:new) - expect(Issues::CloneService).not_to receive(:new) - - update_issue(target_clone_container: target_container, clone_with_notes: true) - end - end - end end end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 6195574b2fd..273934357f8 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -1967,9 +1967,7 @@ - './ee/spec/services/ee/issuable/destroy_service_spec.rb' - './ee/spec/services/ee/issue_links/create_service_spec.rb' - './ee/spec/services/ee/issues/after_create_service_spec.rb' -- './ee/spec/services/ee/issues/clone_service_spec.rb' - './ee/spec/services/ee/issues/create_service_spec.rb' -- './ee/spec/services/ee/issues/move_service_spec.rb' - './ee/spec/services/ee/issues/update_service_spec.rb' - './ee/spec/services/ee/keys/destroy_service_spec.rb' - './ee/spec/services/ee/labels/promote_service_spec.rb' @@ -7272,7 +7270,6 @@ - './spec/services/deployments/update_environment_service_spec.rb' - './spec/services/deployments/update_service_spec.rb' - './spec/services/design_management/copy_design_collection/copy_service_spec.rb' -- './spec/services/design_management/copy_design_collection/queue_service_spec.rb' - './spec/services/design_management/delete_designs_service_spec.rb' - './spec/services/design_management/design_user_notes_count_service_spec.rb' - './spec/services/design_management/generate_image_versions_service_spec.rb' diff --git a/spec/support/shared_contexts/graphql/requests/work_items_shared_context.rb b/spec/support/shared_contexts/graphql/requests/work_items_shared_context.rb new file mode 100644 index 00000000000..3c2b69ea4d7 --- /dev/null +++ b/spec/support/shared_contexts/graphql/requests/work_items_shared_context.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +RSpec.shared_context 'with work items list request' do + include GraphqlHelpers + + let_it_be(:group) { create(:group, :public) } + let_it_be(:project) { create(:project, :repository, :public, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:reporter) { create(:user, reporter_of: [group, project]) } + let_it_be(:current_user) { user } + + let(:item_filter_params) { {} } + + let(:fields) do + <<~QUERY + nodes { + #{all_graphql_fields_for('workItems'.classify, max_depth: 2)} + } + QUERY + end +end diff --git a/spec/tooling/lib/tooling/glci/failure_categories/download_job_trace_spec.rb b/spec/tooling/lib/tooling/glci/failure_categories/download_job_trace_spec.rb index f16901dd6a8..8cd4d8c4f74 100644 --- a/spec/tooling/lib/tooling/glci/failure_categories/download_job_trace_spec.rb +++ b/spec/tooling/lib/tooling/glci/failure_categories/download_job_trace_spec.rb @@ -13,8 +13,15 @@ RSpec.describe Tooling::Glci::FailureCategories::DownloadJobTrace, feature_categ let(:job_id) { '67890' } let(:access_token) { 'fake_token' } let(:job_status) { 'failed' } - let(:trace_content) { "This is the job trace content\nWith multiple lines\nOf output" } + let(:trace_marker) { 'failure-analyzer' } + let(:trace_content_without_marker) { "This is the job trace content\nWith multiple lines\nOf output" } + let(:trace_content_with_marker) do + "#{trace_content_without_marker}\nsection_start:1234567890:#{trace_marker}\nReport failure category" + end + let(:output_file) { 'test_trace.log' } + let(:max_attempts) { 3 } + let(:retry_delay) { 0.01 } # Use small delay in tests subject(:downloader) do described_class.new( @@ -22,11 +29,18 @@ RSpec.describe Tooling::Glci::FailureCategories::DownloadJobTrace, feature_categ project_id: project_id, job_id: job_id, access_token: access_token, - job_status: job_status + job_status: job_status, + trace_marker: trace_marker, + max_attempts: max_attempts, + retry_delay: retry_delay ) end before do + # Silence outputs to stdout by default + allow(downloader).to receive(:puts) + allow(downloader).to receive(:warn) + # Clear all relevant environment variables to avoid external state influence stub_env('CI_API_V4_URL', nil) stub_env('CI_PROJECT_ID', nil) @@ -34,10 +48,6 @@ RSpec.describe Tooling::Glci::FailureCategories::DownloadJobTrace, feature_categ stub_env('PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE', nil) stub_env('CI_JOB_STATUS', nil) - stub_request(:get, "#{api_url}/projects/#{project_id}/jobs/#{job_id}/trace") - .with(headers: { 'PRIVATE-TOKEN' => access_token }) - .to_return(status: 200, body: trace_content) - FileUtils.rm_f(output_file) end @@ -169,11 +179,86 @@ RSpec.describe Tooling::Glci::FailureCategories::DownloadJobTrace, feature_categ end describe '#download' do - context 'when job status is failed' do - it 'downloads the trace file' do + shared_examples 'writes the expected trace to disk' do + it 'writes the expected trace to disk' do expect(downloader.download(output_file: output_file)).to eq(output_file) - expect(File.exist?(output_file)).to be true - expect(File.read(output_file)).to eq(trace_content) + expect(File).to exist(output_file) + expect(File.read(output_file)).to eq(expected_content) + end + end + + context 'when job status is failed' do + context 'when we find the marker in the trace on first attempt' do + before do + stub_request(:get, "#{api_url}/projects/#{project_id}/jobs/#{job_id}/trace") + .with(headers: { 'PRIVATE-TOKEN' => access_token }) + .to_return(status: 200, body: trace_content_with_marker) + end + + let(:expected_content) { trace_content_with_marker } + + include_examples 'writes the expected trace to disk' + + it 'verifies the trace contains the marker' do + allow(downloader).to receive(:puts).and_call_original + + expect { downloader.download(output_file: output_file) } + .to output(/\[DownloadJobTrace\] Trace marker found/).to_stdout + end + + it 'only attempts to download once' do + expect(downloader).to receive(:fetch_trace).once.and_call_original + + downloader.download(output_file: output_file) + + expect(WebMock).to have_requested(:get, "#{api_url}/projects/#{project_id}/jobs/#{job_id}/trace").once + end + end + + context 'when we do not find the marker in the trace on first attempt, but we find it on final attempt' do + before do + stub_request(:get, "#{api_url}/projects/#{project_id}/jobs/#{job_id}/trace") + .with(headers: { 'PRIVATE-TOKEN' => access_token }) + .to_return({ status: 200, body: trace_content_without_marker }, + { status: 200, body: trace_content_without_marker }, + { status: 200, body: trace_content_with_marker }) + end + + let(:expected_content) { trace_content_with_marker } + + include_examples 'writes the expected trace to disk' + + it 'makes multiple requests until marker is found' do + expect(downloader).to receive(:fetch_trace).exactly(3).times.and_call_original + + downloader.download(output_file: output_file) + + expect(WebMock).to have_requested(:get, "#{api_url}/projects/#{project_id}/jobs/#{job_id}/trace").times(3) + end + end + + context 'when we do not find the marker after all attempts' do + before do + stub_request(:get, "#{api_url}/projects/#{project_id}/jobs/#{job_id}/trace") + .with(headers: { 'PRIVATE-TOKEN' => access_token }) + .to_return(status: 200, body: trace_content_without_marker) + end + + let(:expected_content) { trace_content_without_marker } + + include_examples 'writes the expected trace to disk' + + it 'makes max_attempts requests and gives a warning' do + allow(downloader).to receive(:warn).and_call_original + + expect { downloader.download(output_file: output_file) } + .to output( + /\[DownloadJobTrace\] Could not verify we have the trace we need after #{max_attempts} attempts/ + ).to_stderr + + expect(WebMock).to have_requested(:get, "#{api_url}/projects/#{project_id}/jobs/#{job_id}/trace") + .times(max_attempts) + end end end @@ -181,11 +266,13 @@ RSpec.describe Tooling::Glci::FailureCategories::DownloadJobTrace, feature_categ let(:job_status) { 'success' } it 'skips downloading the trace file' do + allow(downloader).to receive(:puts).and_call_original + result = "" expect do result = downloader.download(output_file: output_file) - end.to output("[DownloadJobTrace] Job did not fail: exiting early (status: success)\n").to_stdout + end.to output(/\[DownloadJobTrace\] Job did not fail: exiting early/).to_stdout expect(result).to be_nil expect(File.exist?(output_file)).to be false @@ -205,20 +292,85 @@ RSpec.describe Tooling::Glci::FailureCategories::DownloadJobTrace, feature_categ end context 'with environment variables' do + let(:default_downloader) { described_class.new } + before do stub_env('CI_API_V4_URL', api_url) stub_env('CI_PROJECT_ID', project_id) stub_env('CI_JOB_ID', job_id) stub_env('PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE', access_token) stub_env('CI_JOB_STATUS', job_status) - end - let(:default_downloader) { described_class.new } + stub_request(:get, "#{api_url}/projects/#{project_id}/jobs/#{job_id}/trace") + .with(headers: { 'PRIVATE-TOKEN' => access_token }) + .to_return(status: 200, body: trace_content_with_marker) + + # Silence outputs to stdout by default + allow(default_downloader).to receive(:puts) + end it 'uses environment variables when no parameters are provided' do expect(default_downloader.download(output_file: output_file)).to eq(output_file) - expect(File.exist?(output_file)).to be true - expect(File.read(output_file)).to eq(trace_content) + expect(File).to exist(output_file) + expect(File.read(output_file)).to eq(trace_content_with_marker) + end + end + + context 'with custom trace marker' do + let(:custom_marker) { 'CUSTOM_MARKER_12345' } + let(:custom_trace_with_marker) { "#{trace_content_without_marker}\nCUSTOM_MARKER_12345\nEnd of trace" } + + before do + stub_request(:get, "#{api_url}/projects/#{project_id}/jobs/#{job_id}/trace") + .with(headers: { 'PRIVATE-TOKEN' => access_token }) + .to_return(status: 200, body: custom_trace_with_marker) + end + + it 'verifies trace using the custom marker' do + custom_downloader = described_class.new( + api_url: api_url, + project_id: project_id, + job_id: job_id, + access_token: access_token, + job_status: job_status, + trace_marker: custom_marker, + max_attempts: max_attempts, + retry_delay: retry_delay + ) + + # Silence outputs to stdout by default + allow(custom_downloader).to receive(:puts) + + expect(custom_downloader.download(output_file: output_file)).to eq(output_file) + expect(File).to exist(output_file) + expect(File.read(output_file)).to eq(custom_trace_with_marker) + end + end + end + + describe 'trace verification' do + context 'when trace is nil' do + it 'does not have the marker' do + expect(downloader.send(:has_marker?, nil)).to be false + end + end + + context 'when trace is empty' do + it 'does not have the marker' do + expect(downloader.send(:has_marker?, '')).to be false + end + end + + context 'when trace does not contain the marker' do + it 'does not have the marker' do + expect(downloader.send(:has_marker?, 'Some content without the marker')).to be false + end + end + + context 'when trace contains the marker' do + it 'has the marker' do + expect(downloader.send(:has_marker?, + "Some content\nsection_start:1234567890:#{trace_marker}\nMore content")).to be true end end end diff --git a/spec/tooling/lib/tooling/glci/failure_categories/job_trace_to_failure_category_spec.rb b/spec/tooling/lib/tooling/glci/failure_categories/job_trace_to_failure_category_spec.rb index c85cc5e3324..d7adbc3a315 100644 --- a/spec/tooling/lib/tooling/glci/failure_categories/job_trace_to_failure_category_spec.rb +++ b/spec/tooling/lib/tooling/glci/failure_categories/job_trace_to_failure_category_spec.rb @@ -69,16 +69,6 @@ RSpec.describe Tooling::Glci::FailureCategories::JobTraceToFailureCategory, feat expect(result).to be_nil end - - it 'outputs the trace it used for debugging purposes' do - result = "" - - expect do - result = parser.process(trace_path) - end.to output(/Job trace we used:.+this trace should not be matched with any failure category/m).to_stderr - - expect(result).to be_nil - end end describe 'single-line patterns' do diff --git a/tooling/lib/tooling/glci/failure_categories/download_job_trace.rb b/tooling/lib/tooling/glci/failure_categories/download_job_trace.rb index a8e89f63cb5..23a32f17586 100755 --- a/tooling/lib/tooling/glci/failure_categories/download_job_trace.rb +++ b/tooling/lib/tooling/glci/failure_categories/download_job_trace.rb @@ -20,17 +20,27 @@ module Tooling module Glci module FailureCategories class DownloadJobTrace + DEFAULT_TRACE_MARKER = 'failure-analyzer' + DEFAULT_MAX_ATTEMPTS = 5 + DEFAULT_RETRY_DELAY_SECONDS = 10 + def initialize( api_url: ENV['CI_API_V4_URL'], project_id: ENV['CI_PROJECT_ID'], job_id: ENV['CI_JOB_ID'], access_token: ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE'], - job_status: ENV['CI_JOB_STATUS']) - @api_url = api_url + job_status: ENV['CI_JOB_STATUS'], + trace_marker: DEFAULT_TRACE_MARKER, + max_attempts: DEFAULT_MAX_ATTEMPTS, + retry_delay: DEFAULT_RETRY_DELAY_SECONDS) + @api_url = api_url @project_id = project_id - @job_id = job_id + @job_id = job_id @access_token = access_token @job_status = job_status + @trace_marker = trace_marker + @max_attempts = max_attempts + @retry_delay = retry_delay validate_required_parameters! end @@ -41,6 +51,44 @@ module Tooling return end + trace_content = download_trace_with_retry + return unless trace_content + + File.write(output_file, trace_content) + puts "[DownloadJobTrace] Job trace saved to #{output_file}" + + output_file + end + + private + + def download_trace_with_retry + attempt = 0 + + while attempt < @max_attempts + attempt += 1 + puts "[DownloadJobTrace] Downloading job trace (attempt #{attempt}/#{@max_attempts})" + + trace_content = fetch_trace + return trace_content if has_marker?(trace_content) + + sleep @retry_delay if attempt < @max_attempts + end + + warn "[DownloadJobTrace] Could not verify we have the trace we need after #{@max_attempts} attempts" + trace_content + end + + def has_marker?(trace_content) + return false if trace_content.nil? || trace_content.empty? + + has_marker = trace_content.match?(/#{@trace_marker}/) + puts "[DownloadJobTrace] Trace marker #{has_marker ? 'found' : 'not found'}" + + has_marker + end + + def fetch_trace uri = URI.parse("#{@api_url}/projects/#{@project_id}/jobs/#{@job_id}/trace") request = Net::HTTP::Get.new(uri) request['PRIVATE-TOKEN'] = @access_token @@ -53,13 +101,9 @@ module Tooling raise "[DownloadJobTrace] Failed to download job trace: #{response.code} #{response.message}" end - File.write(output_file, response.body) - - output_file + response.body end - private - def validate_required_parameters! missing_params = [] missing_params << 'api_url' if @api_url.nil? || @api_url.empty? diff --git a/tooling/lib/tooling/glci/failure_categories/job_trace_to_failure_category.rb b/tooling/lib/tooling/glci/failure_categories/job_trace_to_failure_category.rb index 32ac9110dae..2a0f2145fd5 100755 --- a/tooling/lib/tooling/glci/failure_categories/job_trace_to_failure_category.rb +++ b/tooling/lib/tooling/glci/failure_categories/job_trace_to_failure_category.rb @@ -46,8 +46,6 @@ module Tooling end warn "[JobTraceToFailureCategory] Error: Could not find any failure category" - warn "Job trace we used:" - warn trace nil end diff --git a/vendor/project_templates/jekyll.tar.gz b/vendor/project_templates/jekyll.tar.gz index a00e043bff0..7ab1c7eddfe 100644 Binary files a/vendor/project_templates/jekyll.tar.gz and b/vendor/project_templates/jekyll.tar.gz differ diff --git a/yarn.lock b/yarn.lock index fa90f7c6461..eec51775523 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4689,6 +4689,17 @@ babel-plugin-istanbul@^6.1.1: istanbul-lib-instrument "^5.0.4" test-exclude "^6.0.0" +babel-plugin-istanbul@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz#629a178f63b83dc9ecee46fd20266283b1f11280" + integrity sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.3" + istanbul-lib-instrument "^6.0.2" + test-exclude "^6.0.0" + babel-plugin-jest-hoist@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" @@ -9406,7 +9417,7 @@ istanbul-lib-instrument@^5.0.4: istanbul-lib-coverage "^3.2.0" semver "^6.3.0" -istanbul-lib-instrument@^6.0.0: +istanbul-lib-instrument@^6.0.0, istanbul-lib-instrument@^6.0.2: version "6.0.3" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==