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==