diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 12:10:43 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-20 12:10:43 +0000 |
commit | bf18f3295b550c564086efd0a32d9a25435ce216 (patch) | |
tree | 9ea92eefd45aa38a15152fb28c24d526c1525a5f | |
parent | 3f96425b0b9f0b4885b70db01dcd76b311ea87ab (diff) | |
download | gitlab-ce-bf18f3295b550c564086efd0a32d9a25435ce216.tar.gz |
Add latest changes from gitlab-org/gitlab@master
98 files changed, 2589 insertions, 1122 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 1a1c67bf572..9d306eed77c 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -7,7 +7,7 @@ include: - local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml - local: .gitlab/ci/package-and-test/variables.gitlab-ci.yml - project: gitlab-org/quality/pipeline-common - ref: 1.3.0 + ref: 1.4.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index 0214f5ef3f2..af7c0231a74 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -1,6 +1,6 @@ include: - project: gitlab-org/quality/pipeline-common - ref: 1.3.0 + ref: 1.4.0 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml diff --git a/.haml-lint.yml b/.haml-lint.yml index 60f86eb4158..3655ca44172 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -110,6 +110,7 @@ linters: - Cop/LineBreakAfterGuardClauses - Cop/ProjectPathHelper - Gitlab/FeatureAvailableUsage + - Gitlab/Json - GitlabSecurity/PublicSend - Layout/EmptyLineAfterGuardClause - Layout/LeadingCommentSpace diff --git a/.rubocop_todo/gitlab/json.yml b/.rubocop_todo/gitlab/json.yml new file mode 100644 index 00000000000..9a946f11f84 --- /dev/null +++ b/.rubocop_todo/gitlab/json.yml @@ -0,0 +1,494 @@ +--- +# Cop supports --auto-correct. +Gitlab/Json: + Details: grace period + Exclude: + - 'app/controllers/admin/application_settings_controller.rb' + - 'app/controllers/concerns/authenticates_with_two_factor.rb' + - 'app/controllers/projects/commit_controller.rb' + - 'app/controllers/projects/google_cloud/configuration_controller.rb' + - 'app/controllers/projects/google_cloud/databases_controller.rb' + - 'app/controllers/projects/google_cloud/deployments_controller.rb' + - 'app/controllers/projects/google_cloud/gcp_regions_controller.rb' + - 'app/controllers/projects/google_cloud/service_accounts_controller.rb' + - 'app/controllers/projects/graphs_controller.rb' + - 'app/controllers/projects/merge_requests_controller.rb' + - 'app/controllers/projects/notes_controller.rb' + - 'app/controllers/projects/settings/ci_cd_controller.rb' + - 'app/controllers/projects/templates_controller.rb' + - 'app/controllers/projects_controller.rb' + - 'app/controllers/search_controller.rb' + - 'app/helpers/access_tokens_helper.rb' + - 'app/helpers/application_settings_helper.rb' + - 'app/helpers/breadcrumbs_helper.rb' + - 'app/helpers/ci/builds_helper.rb' + - 'app/helpers/ci/pipelines_helper.rb' + - 'app/helpers/compare_helper.rb' + - 'app/helpers/emails_helper.rb' + - 'app/helpers/environment_helper.rb' + - 'app/helpers/groups_helper.rb' + - 'app/helpers/ide_helper.rb' + - 'app/helpers/integrations_helper.rb' + - 'app/helpers/invite_members_helper.rb' + - 'app/helpers/issuables_description_templates_helper.rb' + - 'app/helpers/issuables_helper.rb' + - 'app/helpers/jira_connect_helper.rb' + - 'app/helpers/learn_gitlab_helper.rb' + - 'app/helpers/namespaces_helper.rb' + - 'app/helpers/notes_helper.rb' + - 'app/helpers/operations_helper.rb' + - 'app/helpers/packages_helper.rb' + - 'app/helpers/projects/project_members_helper.rb' + - 'app/helpers/projects_helper.rb' + - 'app/helpers/search_helper.rb' + - 'app/helpers/terms_helper.rb' + - 'app/helpers/users_helper.rb' + - 'app/mailers/emails/members.rb' + - 'app/models/concerns/redis_cacheable.rb' + - 'app/models/diff_discussion.rb' + - 'app/models/integrations/assembla.rb' + - 'app/models/integrations/pivotaltracker.rb' + - 'app/models/integrations/pumble.rb' + - 'app/models/integrations/unify_circuit.rb' + - 'app/models/integrations/webex_teams.rb' + - 'app/models/merge_request_diff_commit.rb' + - 'app/presenters/packages/composer/packages_presenter.rb' + - 'app/presenters/projects/security/configuration_presenter.rb' + - 'app/services/bulk_imports/lfs_objects_export_service.rb' + - 'app/services/ci/pipeline_artifacts/coverage_report_service.rb' + - 'app/services/ci/pipeline_artifacts/create_code_quality_mr_diff_report_service.rb' + - 'app/services/ci/pipeline_trigger_service.rb' + - 'app/services/ci/register_job_service.rb' + - 'app/services/google_cloud/create_service_accounts_service.rb' + - 'app/services/google_cloud/setup_cloudsql_instance_service.rb' + - 'app/services/merge_requests/add_context_service.rb' + - 'app/services/projects/lfs_pointers/lfs_download_link_list_service.rb' + - 'app/services/service_ping/submit_service.rb' + - 'app/workers/google_cloud/create_cloudsql_instance_worker.rb' + - 'config/initializers/rack_multipart_patch.rb' + - 'db/migrate/20210305031822_create_dast_site_profile_variables.rb' + - 'db/migrate/20210317035357_create_dast_profiles_pipelines.rb' + - 'db/migrate/20210412111213_create_security_orchestration_policy_rule_schedule.rb' + - 'db/migrate/20210423054022_create_dast_site_profiles_pipelines.rb' + - 'db/migrate/20210604032738_create_dast_site_profiles_builds.rb' + - 'db/migrate/20210604051330_create_dast_scanner_profiles_builds.rb' + - 'db/migrate/20210713123345_create_dast_profile_schedule.rb' + - 'db/post_migrate/20210311120155_backfill_events_id_for_bigint_conversion.rb' + - 'db/post_migrate/20210311120156_backfill_push_event_payload_event_id_for_bigint_conversion.rb' + - 'db/post_migrate/20210415101228_backfill_ci_build_needs_for_bigint_conversion.rb' + - 'db/post_migrate/20210420121149_backfill_conversion_of_ci_job_artifacts.rb' + - 'db/post_migrate/20210422023046_backfill_ci_sources_pipelines_source_job_id_for_bigint_conversion.rb' + - 'db/post_migrate/20210615234935_fix_batched_migrations_old_format_job_arguments.rb' + - 'db/post_migrate/20221006172302_adjust_task_note_rename_background_migration_values.rb' + - 'ee/app/controllers/admin/geo/nodes_controller.rb' + - 'ee/app/controllers/ee/admin/application_settings_controller.rb' + - 'ee/app/controllers/ee/search_controller.rb' + - 'ee/app/controllers/subscriptions_controller.rb' + - 'ee/app/graphql/types/json_string_type.rb' + - 'ee/app/helpers/billing_plans_helper.rb' + - 'ee/app/helpers/ee/environments_helper.rb' + - 'ee/app/helpers/ee/geo_helper.rb' + - 'ee/app/helpers/ee/groups/analytics/cycle_analytics_helper.rb' + - 'ee/app/helpers/ee/invite_members_helper.rb' + - 'ee/app/helpers/ee/operations_helper.rb' + - 'ee/app/helpers/ee/projects/pipeline_helper.rb' + - 'ee/app/helpers/ee/projects_helper.rb' + - 'ee/app/helpers/ee/security_orchestration_helper.rb' + - 'ee/app/helpers/groups/ldap_sync_helper.rb' + - 'ee/app/helpers/groups/security_features_helper.rb' + - 'ee/app/helpers/incident_management/oncall_schedule_helper.rb' + - 'ee/app/helpers/projects/on_demand_scans_helper.rb' + - 'ee/app/helpers/projects/security/dast_profiles_helper.rb' + - 'ee/app/helpers/security_helper.rb' + - 'ee/app/helpers/subscriptions_helper.rb' + - 'ee/app/helpers/users/identity_verification_helper.rb' + - 'ee/app/helpers/vulnerabilities_helper.rb' + - 'ee/app/models/product_analytics/jitsu_authentication.rb' + - 'ee/app/presenters/epic_presenter.rb' + - 'ee/app/services/arkose/blocked_users_report_service.rb' + - 'ee/app/services/elastic/indexing_control_service.rb' + - 'ee/app/services/elastic/process_bookkeeping_service.rb' + - 'ee/app/services/security/token_revocation_service.rb' + - 'ee/app/services/status_page/publish_base_service.rb' + - 'ee/app/services/upcoming_reconciliations/update_service.rb' + - 'ee/app/services/vulnerabilities/create_service_base.rb' + - 'ee/app/workers/concerns/elastic/migration_state.rb' + - 'ee/app/workers/sync_seat_link_request_worker.rb' + - 'ee/db/fixtures/development/20_vulnerabilities.rb' + - 'ee/lib/api/analytics/product_analytics.rb' + - 'ee/lib/ee/gitlab/background_migration/update_vulnerability_occurrences_location.rb' + - 'ee/lib/gitlab/elastic/indexer.rb' + - 'ee/lib/gitlab/geo/signed_data.rb' + - 'ee/lib/gitlab/subscription_portal/clients/graphql.rb' + - 'ee/lib/gitlab/subscription_portal/clients/rest.rb' + - 'ee/lib/slack/api.rb' + - 'ee/lib/tasks/gitlab/elastic.rake' + - 'ee/lib/tasks/gitlab/spdx.rake' + - 'ee/spec/controllers/admin/application_settings_controller_spec.rb' + - 'ee/spec/controllers/countries_controller_spec.rb' + - 'ee/spec/controllers/country_states_controller_spec.rb' + - 'ee/spec/controllers/ee/search_controller_spec.rb' + - 'ee/spec/controllers/groups/analytics/cycle_analytics_controller_spec.rb' + - 'ee/spec/controllers/groups/security/policies_controller_spec.rb' + - 'ee/spec/controllers/projects/integrations/jira/issues_controller_spec.rb' + - 'ee/spec/controllers/subscriptions_controller_spec.rb' + - 'ee/spec/factories/vulnerabilities/findings.rb' + - 'ee/spec/features/admin/subscriptions/admin_views_subscription_spec.rb' + - 'ee/spec/features/billings/billing_plans_spec.rb' + - 'ee/spec/features/billings/extend_reactivate_trial_spec.rb' + - 'ee/spec/features/billings/qrtly_reconciliation_alert_spec.rb' + - 'ee/spec/features/projects/integrations/jira_issues_list_spec.rb' + - 'ee/spec/features/projects/integrations/user_activates_github_spec.rb' + - 'ee/spec/features/projects/integrations/user_activates_jira_spec.rb' + - 'ee/spec/frontend/fixtures/dast_profiles.rb' + - 'ee/spec/frontend/fixtures/epic.rb' + - 'ee/spec/graphql/api/vulnerabilities_spec.rb' + - 'ee/spec/graphql/types/json_string_type_spec.rb' + - 'ee/spec/helpers/ee/groups/group_members_helper_spec.rb' + - 'ee/spec/helpers/ee/projects/pipeline_helper_spec.rb' + - 'ee/spec/helpers/ee/security_orchestration_helper_spec.rb' + - 'ee/spec/helpers/incident_management/oncall_schedule_helper_spec.rb' + - 'ee/spec/helpers/projects/on_demand_scans_helper_spec.rb' + - 'ee/spec/helpers/projects/security/dast_profiles_helper_spec.rb' + - 'ee/spec/helpers/users/identity_verification_helper_spec.rb' + - 'ee/spec/lib/ee/gitlab/background_migration/drop_invalid_remediations_spec.rb' + - 'ee/spec/lib/ee/gitlab/background_migration/update_vulnerability_occurrences_location_spec.rb' + - 'ee/spec/lib/gitlab/analytics/cycle_analytics/request_params_spec.rb' + - 'ee/spec/lib/gitlab/ci/parsers/license_compliance/license_scanning_spec.rb' + - 'ee/spec/lib/gitlab/ci/parsers/security/dast_spec.rb' + - 'ee/spec/lib/gitlab/ci/parsers/security/dependency_scanning_spec.rb' + - 'ee/spec/lib/gitlab/elastic/bulk_indexer_spec.rb' + - 'ee/spec/lib/gitlab/elastic/indexer_spec.rb' + - 'ee/spec/lib/gitlab/geo/replication/blob_downloader_spec.rb' + - 'ee/spec/lib/gitlab/tracking/standard_context_spec.rb' + - 'ee/spec/lib/slack/api_spec.rb' + - 'ee/spec/migrations/update_vulnerability_occurrences_location_spec.rb' + - 'ee/spec/models/ee/integrations/jira_spec.rb' + - 'ee/spec/models/gitlab/seat_link_data_spec.rb' + - 'ee/spec/models/group_member_spec.rb' + - 'ee/spec/models/integrations/github/status_notifier_spec.rb' + - 'ee/spec/models/integrations/github_spec.rb' + - 'ee/spec/models/license_spec.rb' + - 'ee/spec/models/product_analytics/jitsu_authentication_spec.rb' + - 'ee/spec/models/vulnerabilities/finding_spec.rb' + - 'ee/spec/presenters/audit_event_presenter_spec.rb' + - 'ee/spec/requests/api/analytics/product_analytics_spec.rb' + - 'ee/spec/requests/api/experiments_spec.rb' + - 'ee/spec/requests/api/geo_spec.rb' + - 'ee/spec/requests/api/graphql/mutations/alert_management/http_integration/create_spec.rb' + - 'ee/spec/requests/api/graphql/mutations/alert_management/http_integration/update_spec.rb' + - 'ee/spec/requests/api/graphql/mutations/vulnerabilities/create_external_issue_link_spec.rb' + - 'ee/spec/requests/api/graphql/project/alert_management/http_integrations_spec.rb' + - 'ee/spec/requests/api/graphql/vulnerabilities/external_issue_links_spec.rb' + - 'ee/spec/requests/api/graphql/vulnerabilities/location_spec.rb' + - 'ee/spec/requests/api/integrations/slack/events_spec.rb' + - 'ee/spec/requests/api/releases_spec.rb' + - 'ee/spec/requests/api/settings_spec.rb' + - 'ee/spec/requests/git_http_geo_spec.rb' + - 'ee/spec/requests/projects/on_demand_scans_controller_spec.rb' + - 'ee/spec/requests/projects/security/policies_controller_spec.rb' + - 'ee/spec/requests/users/identity_verification_controller_spec.rb' + - 'ee/spec/serializers/clusters/environment_entity_spec.rb' + - 'ee/spec/serializers/clusters/environment_serializer_spec.rb' + - 'ee/spec/serializers/dependency_list_serializer_spec.rb' + - 'ee/spec/serializers/epics/related_epic_entity_spec.rb' + - 'ee/spec/serializers/evidences/evidence_entity_spec.rb' + - 'ee/spec/serializers/issue_serializer_spec.rb' + - 'ee/spec/serializers/licenses_list_serializer_spec.rb' + - 'ee/spec/serializers/member_entity_spec.rb' + - 'ee/spec/serializers/member_user_entity_spec.rb' + - 'ee/spec/serializers/status_page/incident_entity_spec.rb' + - 'ee/spec/serializers/status_page/incident_serializer_spec.rb' + - 'ee/spec/serializers/test_reports_comparer_serializer_spec.rb' + - 'ee/spec/services/arkose/blocked_users_report_service_spec.rb' + - 'ee/spec/services/arkose/token_verification_service_spec.rb' + - 'ee/spec/services/gitlab_subscriptions/fetch_subscription_plans_service_spec.rb' + - 'ee/spec/services/integrations/slack_events/app_home_opened_service_spec.rb' + - 'ee/spec/services/jira/requests/issues/list_service_spec.rb' + - 'ee/spec/services/projects/slack_application_install_service_spec.rb' + - 'ee/spec/services/security/token_revocation_service_spec.rb' + - 'ee/spec/support/helpers/subscription_portal_helpers.rb' + - 'ee/spec/support/shared_examples/controllers/cluster_metrics_shared_examples.rb' + - 'ee/spec/support/shared_examples/requests/api/project_approval_rules_api_shared_examples.rb' + - 'ee/spec/support/shared_examples/status_page/publish_shared_examples.rb' + - 'ee/spec/tasks/gitlab/spdx_rake_spec.rb' + - 'ee/spec/workers/audit_events/audit_event_streaming_worker_spec.rb' + - 'ee/spec/workers/scan_security_report_secrets_worker_spec.rb' + - 'ee/spec/workers/sync_seat_link_request_worker_spec.rb' + - 'ee/spec/workers/vulnerability_exports/export_worker_spec.rb' + - 'lib/api/api.rb' + - 'lib/api/feature_flags_user_lists.rb' + - 'lib/api/helpers.rb' + - 'lib/api/terraform/state.rb' + - 'lib/atlassian/jira_connect/client.rb' + - 'lib/atlassian/jira_connect/serializers/base_entity.rb' + - 'lib/backup/gitaly_backup.rb' + - 'lib/bitbucket_server/client.rb' + - 'lib/bulk_imports/clients/graphql.rb' + - 'lib/error_tracking/sentry_client.rb' + - 'lib/gitlab/alert_management/payload/prometheus.rb' + - 'lib/gitlab/analytics/cycle_analytics/request_params.rb' + - 'lib/gitlab/auth/otp/strategies/forti_authenticator/manual_otp.rb' + - 'lib/gitlab/auth/otp/strategies/forti_authenticator/push_otp.rb' + - 'lib/gitlab/auth/otp/strategies/forti_token_cloud.rb' + - 'lib/gitlab/background_migration/fix_vulnerability_occurrences_with_hashes_as_raw_metadata.rb' + - 'lib/gitlab/bitbucket_import/importer.rb' + - 'lib/gitlab/bitbucket_server_import/importer.rb' + - 'lib/gitlab/chat/responder/mattermost.rb' + - 'lib/gitlab/chat/responder/slack.rb' + - 'lib/gitlab/chat_name_token.rb' + - 'lib/gitlab/ci/ansi2html.rb' + - 'lib/gitlab/ci/ansi2json/state.rb' + - 'lib/gitlab/ci/build/releaser.rb' + - 'lib/gitlab/ci/config/external/mapper.rb' + - 'lib/gitlab/ci/pipeline/chain/validate/external.rb' + - 'lib/gitlab/ci/reports/security/finding.rb' + - 'lib/gitlab/composer/cache.rb' + - 'lib/gitlab/database/background_migration/batched_migration.rb' + - 'lib/gitlab/database/background_migration_job.rb' + - 'lib/gitlab/database/migration_helpers.rb' + - 'lib/gitlab/database/migrations/instrumentation.rb' + - 'lib/gitlab/database/migrations/runner.rb' + - 'lib/gitlab/database/postgres_hll/buckets.rb' + - 'lib/gitlab/database/reindexing/grafana_notifier.rb' + - 'lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb' + - 'lib/gitlab/diff/highlight_cache.rb' + - 'lib/gitlab/discussions_diff/highlight_cache.rb' + - 'lib/gitlab/external_authorization/client.rb' + - 'lib/gitlab/file_hook.rb' + - 'lib/gitlab/gitaly_client/conflicts_service.rb' + - 'lib/gitlab/graphql/pagination/active_record_array_connection.rb' + - 'lib/gitlab/graphql/pagination/keyset/connection.rb' + - 'lib/gitlab/health_checks/middleware.rb' + - 'lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb' + - 'lib/gitlab/import_export/json/legacy_writer.rb' + - 'lib/gitlab/import_export/json/ndjson_writer.rb' + - 'lib/gitlab/import_export/lfs_saver.rb' + - 'lib/gitlab/jira/http_client.rb' + - 'lib/gitlab/json_cache.rb' + - 'lib/gitlab/legacy_github_import/importer.rb' + - 'lib/gitlab/lfs/client.rb' + - 'lib/gitlab/merge_requests/mergeability/redis_interface.rb' + - 'lib/gitlab/middleware/read_only/controller.rb' + - 'lib/gitlab/patch/hangouts_chat_http_override.rb' + - 'lib/gitlab/puma_logging/json_formatter.rb' + - 'lib/gitlab/sidekiq_config.rb' + - 'lib/gitlab/sidekiq_daemon/monitor.rb' + - 'lib/gitlab/sidekiq_logging/json_formatter.rb' + - 'lib/gitlab/usage/metrics/aggregates/sources/postgres_hll.rb' + - 'lib/gitlab/utils/json_size_estimator.rb' + - 'lib/gitlab/version_info.rb' + - 'lib/gitlab/workhorse.rb' + - 'lib/mattermost/command.rb' + - 'lib/mattermost/team.rb' + - 'lib/microsoft_teams/notifier.rb' + - 'lib/tasks/gitlab/background_migrations.rake' + - 'lib/version_check.rb' + - 'spec/controllers/admin/integrations_controller_spec.rb' + - 'spec/controllers/concerns/product_analytics_tracking_spec.rb' + - 'spec/controllers/groups/settings/integrations_controller_spec.rb' + - 'spec/controllers/jira_connect/subscriptions_controller_spec.rb' + - 'spec/controllers/profiles/personal_access_tokens_controller_spec.rb' + - 'spec/controllers/projects/alerting/notifications_controller_spec.rb' + - 'spec/controllers/projects/jobs_controller_spec.rb' + - 'spec/controllers/projects/merge_requests/drafts_controller_spec.rb' + - 'spec/factories/ci/pipeline_artifacts.rb' + - 'spec/features/dashboard/issues_spec.rb' + - 'spec/features/error_tracking/user_filters_errors_by_status_spec.rb' + - 'spec/features/file_uploads/graphql_add_design_spec.rb' + - 'spec/features/groups/dependency_proxy_for_containers_spec.rb' + - 'spec/features/markdown/copy_as_gfm_spec.rb' + - 'spec/features/markdown/metrics_spec.rb' + - 'spec/features/projects/integrations/user_activates_jira_spec.rb' + - 'spec/features/projects/settings/monitor_settings_spec.rb' + - 'spec/frontend/fixtures/timezones.rb' + - 'spec/helpers/access_tokens_helper_spec.rb' + - 'spec/helpers/breadcrumbs_helper_spec.rb' + - 'spec/helpers/ci/builds_helper_spec.rb' + - 'spec/helpers/environment_helper_spec.rb' + - 'spec/helpers/environments_helper_spec.rb' + - 'spec/helpers/groups/group_members_helper_spec.rb' + - 'spec/helpers/groups_helper_spec.rb' + - 'spec/helpers/ide_helper_spec.rb' + - 'spec/helpers/invite_members_helper_spec.rb' + - 'spec/helpers/issuables_description_templates_helper_spec.rb' + - 'spec/helpers/listbox_helper_spec.rb' + - 'spec/helpers/namespaces_helper_spec.rb' + - 'spec/helpers/projects/project_members_helper_spec.rb' + - 'spec/helpers/projects_helper_spec.rb' + - 'spec/initializers/hangouts_chat_http_override_spec.rb' + - 'spec/lib/api/entities/merge_request_basic_spec.rb' + - 'spec/lib/api/helpers/caching_spec.rb' + - 'spec/lib/api/helpers/common_helpers_spec.rb' + - 'spec/lib/atlassian/jira_connect/client_spec.rb' + - 'spec/lib/atlassian/jira_connect/serializers/build_entity_spec.rb' + - 'spec/lib/atlassian/jira_connect/serializers/deployment_entity_spec.rb' + - 'spec/lib/atlassian/jira_connect/serializers/feature_flag_entity_spec.rb' + - 'spec/lib/atlassian/jira_connect/serializers/repository_entity_spec.rb' + - 'spec/lib/bitbucket_server/connection_spec.rb' + - 'spec/lib/bulk_imports/common/pipelines/lfs_objects_pipeline_spec.rb' + - 'spec/lib/bulk_imports/projects/pipelines/snippets_pipeline_spec.rb' + - 'spec/lib/container_registry/client_spec.rb' + - 'spec/lib/container_registry/gitlab_api_client_spec.rb' + - 'spec/lib/gitlab/background_migration/encrypt_integration_properties_spec.rb' + - 'spec/lib/gitlab/bitbucket_import/importer_spec.rb' + - 'spec/lib/gitlab/chat/responder/mattermost_spec.rb' + - 'spec/lib/gitlab/chat/responder/slack_spec.rb' + - 'spec/lib/gitlab/ci/build/releaser_spec.rb' + - 'spec/lib/gitlab/ci/parsers/accessibility/pa11y_spec.rb' + - 'spec/lib/gitlab/ci/parsers/codequality/code_climate_spec.rb' + - 'spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb' + - 'spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb' + - 'spec/lib/gitlab/ci/parsers/security/common_spec.rb' + - 'spec/lib/gitlab/ci/parsers/test/junit_spec.rb' + - 'spec/lib/gitlab/ci/runner_upgrade_check_spec.rb' + - 'spec/lib/gitlab/composer/cache_spec.rb' + - 'spec/lib/gitlab/composer/version_index_spec.rb' + - 'spec/lib/gitlab/data_builder/pipeline_spec.rb' + - 'spec/lib/gitlab/database/load_balancing/sidekiq_server_middleware_spec.rb' + - 'spec/lib/gitlab/database/postgres_hll/buckets_spec.rb' + - 'spec/lib/gitlab/database/reindexing/grafana_notifier_spec.rb' + - 'spec/lib/gitlab/diff/position_spec.rb' + - 'spec/lib/gitlab/diff/stats_cache_spec.rb' + - 'spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb' + - 'spec/lib/gitlab/error_tracking/context_payload_generator_spec.rb' + - 'spec/lib/gitlab/error_tracking/processor/sidekiq_processor_spec.rb' + - 'spec/lib/gitlab/external_authorization/client_spec.rb' + - 'spec/lib/gitlab/external_authorization/response_spec.rb' + - 'spec/lib/gitlab/file_hook_spec.rb' + - 'spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb' + - 'spec/lib/gitlab/github_import/client_spec.rb' + - 'spec/lib/gitlab/gitlab_import/importer_spec.rb' + - 'spec/lib/gitlab/grape_logging/loggers/exception_logger_spec.rb' + - 'spec/lib/gitlab/harbor/client_spec.rb' + - 'spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb' + - 'spec/lib/gitlab/import_export/json/streaming_serializer_spec.rb' + - 'spec/lib/gitlab/json_cache_spec.rb' + - 'spec/lib/gitlab/legacy_github_import/client_spec.rb' + - 'spec/lib/gitlab/legacy_github_import/importer_spec.rb' + - 'spec/lib/gitlab/lfs/client_spec.rb' + - 'spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb' + - 'spec/lib/gitlab/middleware/multipart_spec.rb' + - 'spec/lib/gitlab/sidekiq_migrate_jobs_spec.rb' + - 'spec/lib/gitlab/tracking/service_ping_context_spec.rb' + - 'spec/lib/gitlab/tracking/standard_context_spec.rb' + - 'spec/lib/gitlab/tracking_spec.rb' + - 'spec/lib/gitlab/usage/service_ping/legacy_metric_timing_decorator_spec.rb' + - 'spec/lib/gitlab/utils/json_size_estimator_spec.rb' + - 'spec/lib/gitlab/version_info_spec.rb' + - 'spec/lib/gitlab/webpack/manifest_spec.rb' + - 'spec/lib/gitlab/workhorse_spec.rb' + - 'spec/lib/gitlab/zentao/client_spec.rb' + - 'spec/lib/grafana/client_spec.rb' + - 'spec/lib/json_web_token/hmac_token_spec.rb' + - 'spec/lib/mattermost/command_spec.rb' + - 'spec/lib/mattermost/team_spec.rb' + - 'spec/lib/microsoft_teams/notifier_spec.rb' + - 'spec/lib/object_storage/direct_upload_spec.rb' + - 'spec/lib/service_ping/devops_report_spec.rb' + - 'spec/lib/version_check_spec.rb' + - 'spec/mailers/notify_spec.rb' + - 'spec/migrations/20220204194347_encrypt_integration_properties_spec.rb' + - 'spec/migrations/20220412143552_consume_remaining_encrypt_integration_property_jobs_spec.rb' + - 'spec/models/blob_viewer/package_json_spec.rb' + - 'spec/models/ci/runner_spec.rb' + - 'spec/models/concerns/prometheus_adapter_spec.rb' + - 'spec/models/concerns/redis_cacheable_spec.rb' + - 'spec/models/concerns/sensitive_serializable_hash_spec.rb' + - 'spec/models/diff_discussion_spec.rb' + - 'spec/models/diff_note_spec.rb' + - 'spec/models/hooks/web_hook_spec.rb' + - 'spec/models/integrations/datadog_spec.rb' + - 'spec/models/integrations/jira_spec.rb' + - 'spec/models/integrations/mattermost_slash_commands_spec.rb' + - 'spec/models/integrations/mock_ci_spec.rb' + - 'spec/models/merge_request_diff_commit_spec.rb' + - 'spec/models/packages/composer/metadatum_spec.rb' + - 'spec/models/terraform/state_spec.rb' + - 'spec/presenters/packages/composer/packages_presenter_spec.rb' + - 'spec/requests/api/ci/runner/jobs_request_post_spec.rb' + - 'spec/requests/api/composer_packages_spec.rb' + - 'spec/requests/api/conan_instance_packages_spec.rb' + - 'spec/requests/api/conan_project_packages_spec.rb' + - 'spec/requests/api/container_registry_event_spec.rb' + - 'spec/requests/api/graphql/mutations/design_management/upload_spec.rb' + - 'spec/requests/api/integrations/jira_connect/subscriptions_spec.rb' + - 'spec/requests/api/internal/base_spec.rb' + - 'spec/requests/api/merge_requests_spec.rb' + - 'spec/requests/api/namespaces_spec.rb' + - 'spec/requests/api/project_snapshots_spec.rb' + - 'spec/requests/projects/incident_management/pagerduty_incidents_spec.rb' + - 'spec/requests/users_controller_spec.rb' + - 'spec/requests/whats_new_controller_spec.rb' + - 'spec/scripts/pipeline_test_report_builder_spec.rb' + - 'spec/serializers/ci/dag_job_entity_spec.rb' + - 'spec/serializers/ci/dag_job_group_entity_spec.rb' + - 'spec/serializers/ci/dag_pipeline_entity_spec.rb' + - 'spec/serializers/ci/dag_pipeline_serializer_spec.rb' + - 'spec/serializers/ci/dag_stage_entity_spec.rb' + - 'spec/serializers/ci/daily_build_group_report_result_serializer_spec.rb' + - 'spec/serializers/ci/lint/result_serializer_spec.rb' + - 'spec/serializers/ci/trigger_entity_spec.rb' + - 'spec/serializers/ci/trigger_serializer_spec.rb' + - 'spec/serializers/diff_line_serializer_spec.rb' + - 'spec/serializers/evidences/evidence_entity_spec.rb' + - 'spec/serializers/feature_flags_client_serializer_spec.rb' + - 'spec/serializers/group_link/group_group_link_entity_spec.rb' + - 'spec/serializers/group_link/group_group_link_serializer_spec.rb' + - 'spec/serializers/group_link/group_link_entity_spec.rb' + - 'spec/serializers/group_link/project_group_link_entity_spec.rb' + - 'spec/serializers/group_link/project_group_link_serializer_spec.rb' + - 'spec/serializers/member_entity_spec.rb' + - 'spec/serializers/member_serializer_spec.rb' + - 'spec/serializers/member_user_entity_spec.rb' + - 'spec/serializers/test_reports_comparer_serializer_spec.rb' + - 'spec/services/ci/runners/process_runner_version_update_service_spec.rb' + - 'spec/services/draft_notes/create_service_spec.rb' + - 'spec/services/error_tracking/issue_details_service_spec.rb' + - 'spec/services/error_tracking/issue_latest_event_service_spec.rb' + - 'spec/services/error_tracking/list_issues_service_spec.rb' + - 'spec/services/git/branch_push_service_spec.rb' + - 'spec/services/jira/requests/projects/list_service_spec.rb' + - 'spec/services/metrics/dashboard/transient_embed_service_spec.rb' + - 'spec/services/packages/composer/create_package_service_spec.rb' + - 'spec/services/packages/rubygems/metadata_extraction_service_spec.rb' + - 'spec/services/projects/container_repository/third_party/cleanup_tags_service_spec.rb' + - 'spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb' + - 'spec/services/service_ping/submit_service_ping_service_spec.rb' + - 'spec/services/webauthn/authenticate_service_spec.rb' + - 'spec/services/webauthn/register_service_spec.rb' + - 'spec/support/frontend_fixtures.rb' + - 'spec/support/google_api/cloud_platform_helpers.rb' + - 'spec/support/helpers/ci_artifact_metadata_generator.rb' + - 'spec/support/helpers/dependency_proxy_helpers.rb' + - 'spec/support/helpers/fake_webauthn_device.rb' + - 'spec/support/helpers/features/two_factor_helpers.rb' + - 'spec/support/helpers/graphql_helpers.rb' + - 'spec/support/helpers/input_helper.rb' + - 'spec/support/helpers/jira_integration_helpers.rb' + - 'spec/support/helpers/kubernetes_helpers.rb' + - 'spec/support/helpers/prometheus_helpers.rb' + - 'spec/support/helpers/sentry_client_helpers.rb' + - 'spec/support/helpers/usage_data_helpers.rb' + - 'spec/support/import_export/configuration_helper.rb' + - 'spec/support/shared_contexts/bulk_imports_requests_shared_context.rb' + - 'spec/support/shared_contexts/features/error_tracking_shared_context.rb' + - 'spec/support/shared_contexts/prometheus/alert_shared_context.rb' + - 'spec/support/shared_contexts/services/projects/container_repository/delete_tags_service_shared_context.rb' + - 'spec/support/shared_examples/blocks_unsafe_serialization_shared_examples.rb' + - 'spec/support/shared_examples/controllers/rate_limited_endpoint_shared_examples.rb' + - 'spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb' + - 'spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb' + - 'spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb' + - 'spec/support/shared_examples/harbor/tags_controller_shared_examples.rb' + - 'spec/support/shared_examples/models/diff_positionable_note_shared_examples.rb' + - 'spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb' + - 'spec/support/shared_examples/requests/rack_attack_shared_examples.rb' + - 'spec/support_specs/helpers/graphql_helpers_spec.rb' + - 'spec/tasks/gitlab/update_templates_rake_spec.rb' + - 'spec/tasks/gitlab/usage_data_rake_spec.rb' + - 'spec/tooling/lib/tooling/kubernetes_client_spec.rb' + - 'spec/tooling/rspec_flaky/listener_spec.rb' + - 'spec/workers/ci/runners/process_runner_version_update_worker_spec.rb' + - 'spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb' + - 'spec/workers/packages/composer/cache_update_worker_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index fe81f057738..bcdd23a89c1 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -334a620a54df6bbb1563c440514e06d7068255e7 +ef8362fdf1c0eca9c73fb0fa4dc5b45c5c7965d8 diff --git a/app/assets/javascripts/groups_projects/components/transfer_locations.vue b/app/assets/javascripts/groups_projects/components/transfer_locations.vue new file mode 100644 index 00000000000..11d7fa8d65b --- /dev/null +++ b/app/assets/javascripts/groups_projects/components/transfer_locations.vue @@ -0,0 +1,233 @@ +<script> +import { + GlAlert, + GlFormGroup, + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, +} from '@gitlab/ui'; +import { debounce } from 'lodash'; +import { s__, __ } from '~/locale'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import currentUserNamespace from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql'; + +export const i18n = { + SELECT_A_NAMESPACE: __('Select a new namespace'), + GROUPS: __('Groups'), + USERS: __('Users'), + ERROR_MESSAGE: s__( + 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.', + ), + ALERT_DISMISS_LABEL: __('Dismiss'), +}; + +export default { + name: 'TransferLocations', + components: { + GlAlert, + GlFormGroup, + GlDropdown, + GlDropdownItem, + GlDropdownSectionHeader, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, + }, + inject: ['resourceId'], + props: { + value: { + type: Object, + required: false, + default: null, + }, + groupTransferLocationsApiMethod: { + type: Function, + required: true, + }, + }, + initialTransferLocationsLoaded: false, + data() { + return { + searchTerm: '', + userTransferLocations: [], + groupTransferLocations: [], + isLoading: false, + isSearchLoading: false, + hasError: false, + page: 1, + totalPages: 1, + }; + }, + computed: { + hasUserTransferLocations() { + return this.userTransferLocations.length; + }, + hasGroupTransferLocations() { + return this.groupTransferLocations.length; + }, + selectedText() { + return this.value?.humanName || i18n.SELECT_A_NAMESPACE; + }, + hasNextPageOfGroups() { + return this.page < this.totalPages; + }, + }, + watch: { + searchTerm() { + this.page = 1; + + this.debouncedSearch(); + }, + }, + methods: { + handleSelect(item) { + this.searchTerm = ''; + this.$emit('input', item); + }, + async handleShow() { + if (this.$options.initialTransferLocationsLoaded) { + return; + } + + this.isLoading = true; + + [this.groupTransferLocations, this.userTransferLocations] = await Promise.all([ + this.getGroupTransferLocations(), + this.getUserTransferLocations(), + ]); + + this.isLoading = false; + this.$options.initialTransferLocationsLoaded = true; + }, + async getGroupTransferLocations() { + try { + const { + data: groupTransferLocations, + headers, + } = await this.groupTransferLocationsApiMethod(this.resourceId, { + page: this.page, + search: this.searchTerm, + }); + + const { totalPages } = parseIntPagination(normalizeHeaders(headers)); + this.totalPages = totalPages; + + return groupTransferLocations.map(({ id, full_name: humanName }) => ({ + id, + humanName, + })); + } catch { + this.handleError(); + + return []; + } + }, + async getUserTransferLocations() { + try { + const { + data: { + currentUser: { namespace }, + }, + } = await this.$apollo.query({ + query: currentUserNamespace, + }); + + if (!namespace) { + return []; + } + + return [ + { + id: getIdFromGraphQLId(namespace.id), + humanName: namespace.fullName, + }, + ]; + } catch { + this.handleError(); + + return []; + } + }, + async handleLoadMoreGroups() { + this.isLoading = true; + this.page += 1; + + const groupTransferLocations = await this.getGroupTransferLocations(); + this.groupTransferLocations.push(...groupTransferLocations); + + this.isLoading = false; + }, + debouncedSearch: debounce(async function debouncedSearch() { + this.isSearchLoading = true; + + this.groupTransferLocations = await this.getGroupTransferLocations(); + + this.isSearchLoading = false; + }, DEBOUNCE_DELAY), + handleError() { + this.hasError = true; + }, + handleAlertDismiss() { + this.hasError = false; + }, + }, + i18n, +}; +</script> +<template> + <div> + <gl-alert + v-if="hasError" + variant="danger" + :dismiss-label="$options.i18n.ALERT_DISMISS_LABEL" + @dismiss="handleAlertDismiss" + >{{ $options.i18n.ERROR_MESSAGE }}</gl-alert + > + <gl-form-group :label="$options.i18n.SELECT_A_NAMESPACE"> + <gl-dropdown :text="selectedText" data-qa-selector="namespaces_list" block @show="handleShow"> + <template #header> + <gl-search-box-by-type + v-model.trim="searchTerm" + :is-loading="isSearchLoading" + data-qa-selector="namespaces_list_search" + /> + </template> + <div + v-if="hasUserTransferLocations" + data-qa-selector="namespaces_list_users" + data-testid="user-transfer-locations" + > + <gl-dropdown-section-header>{{ $options.i18n.USERS }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="item in userTransferLocations" + :key="item.id" + data-qa-selector="namespaces_list_item" + @click="handleSelect(item)" + >{{ item.humanName }}</gl-dropdown-item + > + </div> + <div + v-if="hasGroupTransferLocations" + data-qa-selector="namespaces_list_groups" + data-testid="group-transfer-locations" + > + <gl-dropdown-section-header>{{ $options.i18n.GROUPS }}</gl-dropdown-section-header> + <gl-dropdown-item + v-for="item in groupTransferLocations" + :key="item.id" + data-qa-selector="namespaces_list_item" + @click="handleSelect(item)" + >{{ item.humanName }}</gl-dropdown-item + > + </div> + <gl-loading-icon v-if="isLoading" class="gl-mb-3" size="sm" /> + <gl-intersection-observer v-if="hasNextPageOfGroups" @appear="handleLoadMoreGroups" /> + </gl-dropdown> + </gl-form-group> + </div> +</template> diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql b/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql new file mode 100644 index 00000000000..d350072425b --- /dev/null +++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql @@ -0,0 +1,5 @@ +mutation moveIssue($moveIssueInput: IssueMoveInput!) { + issueMove(input: $moveIssueInput) { + errors + } +} diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue b/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue new file mode 100644 index 00000000000..6e287ac3bb7 --- /dev/null +++ b/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue @@ -0,0 +1,171 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue'; +import createFlash from '~/flash'; +import { logError } from '~/lib/logger'; +import { s__ } from '~/locale'; +import { + WORK_ITEM_TYPE_ENUM_ISSUE, + WORK_ITEM_TYPE_ENUM_INCIDENT, + WORK_ITEM_TYPE_ENUM_TASK, + WORK_ITEM_TYPE_ENUM_TEST_CASE, +} from '~/work_items/constants'; +import issuableEventHub from '~/issues/list/eventhub'; +import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; +import getIssuesCountQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql'; +import moveIssueMutation from './graphql/mutations/move_issue.mutation.graphql'; + +export default { + name: 'MoveIssuesButton', + components: { + IssuableMoveDropdown, + GlAlert, + }, + props: { + projectFullPath: { + type: String, + required: true, + }, + projectsFetchPath: { + type: String, + required: true, + }, + }, + data() { + return { + selectedIssuables: [], + moveInProgress: false, + }; + }, + computed: { + cannotMoveTasksWarningTitle() { + if (this.tasksSelected && this.testCasesSelected) { + return s__('Issues|Tasks and test cases can not be moved.'); + } + + if (this.testCasesSelected) { + return s__('Issues|Test cases can not be moved.'); + } + + return s__('Issues|Tasks can not be moved.'); + }, + issuesSelected() { + return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_ISSUE); + }, + incidentsSelected() { + return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_INCIDENT); + }, + tasksSelected() { + return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_TASK); + }, + testCasesSelected() { + return this.selectedIssuables.some((item) => item.type === WORK_ITEM_TYPE_ENUM_TEST_CASE); + }, + }, + mounted() { + issuableEventHub.$on('issuables:issuableChecked', this.handleIssuableChecked); + }, + beforeDestroy() { + issuableEventHub.$off('issuables:issuableChecked', this.handleIssuableChecked); + }, + methods: { + handleIssuableChecked(issuable, value) { + if (value) { + this.selectedIssuables.push(issuable); + } else { + const index = this.selectedIssuables.indexOf(issuable); + if (index > -1) { + this.selectedIssuables.splice(index, 1); + } + } + }, + moveIssues(targetProject) { + const iids = this.selectedIssuables.reduce((result, issueData) => { + if ( + issueData.type === WORK_ITEM_TYPE_ENUM_ISSUE || + issueData.type === WORK_ITEM_TYPE_ENUM_INCIDENT + ) { + result.push(issueData.iid); + } + return result; + }, []); + + if (iids.length === 0) { + return; + } + + this.moveInProgress = true; + issuableEventHub.$emit('issuables:bulkMoveStarted'); + + const promises = iids.map((id) => { + return this.moveIssue(id, targetProject); + }); + + Promise.all(promises) + .then((promisesResult) => { + let foundError = false; + + for (const promiseResult of promisesResult) { + if (promiseResult.data.issueMove?.errors?.length) { + foundError = true; + logError( + `Error moving issue. Error message: ${promiseResult.data.issueMove.errors[0].message}`, + ); + } + } + + if (!foundError) { + const client = this.$apollo.provider.defaultClient; + client.refetchQueries({ + include: [getIssuesQuery, getIssuesCountQuery], + }); + this.moveInProgress = false; + this.selectedIssuables = []; + issuableEventHub.$emit('issuables:bulkMoveEnded'); + } else { + throw new Error(); + } + }) + .catch(() => { + this.moveInProgress = false; + issuableEventHub.$emit('issuables:bulkMoveEnded'); + + createFlash({ + message: s__(`Issues|There was an error while moving the issues.`), + }); + }); + }, + moveIssue(issueIid, targetProject) { + return this.$apollo.mutate({ + mutation: moveIssueMutation, + variables: { + moveIssueInput: { + projectPath: this.projectFullPath, + iid: issueIid, + targetProjectPath: targetProject.full_path, + }, + }, + }); + }, + }, + i18n: { + dropdownButtonTitle: s__('Issues|Move selected'), + }, +}; +</script> +<template> + <div> + <issuable-move-dropdown + :project-full-path="projectFullPath" + :projects-fetch-path="projectsFetchPath" + :move-in-progress="moveInProgress" + :disabled="!issuesSelected && !incidentsSelected" + :dropdown-header-title="$options.i18n.dropdownButtonTitle" + :dropdown-button-title="$options.i18n.dropdownButtonTitle" + @move-issuable="moveIssues" + /> + <gl-alert v-if="tasksSelected || testCasesSelected" :dismissible="false" variant="warning"> + {{ cannotMoveTasksWarningTitle }} + </gl-alert> + </div> +</template> diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js index 4657771353f..b7cb805ee37 100644 --- a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js +++ b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js @@ -1,6 +1,9 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { gqlClient } from '../../issues/list/graphql'; import StatusDropdown from './components/status_dropdown.vue'; import SubscriptionsDropdown from './components/subscriptions_dropdown.vue'; +import MoveIssuesButton from './components/move_issues_button.vue'; import issuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; @@ -42,3 +45,31 @@ export function initSubscriptionsDropdown() { render: (createElement) => createElement(SubscriptionsDropdown), }); } + +export function initMoveIssuesButton() { + const el = document.querySelector('.js-move-issues'); + + if (!el) { + return null; + } + + const { dataset } = el; + + Vue.use(VueApollo); + const apolloProvider = new VueApollo({ + defaultClient: gqlClient, + }); + + return new Vue({ + el, + name: 'MoveIssuesRoot', + apolloProvider, + render: (createElement) => + createElement(MoveIssuesButton, { + props: { + projectFullPath: dataset.projectFullPath, + projectsFetchPath: dataset.projectsFetchPath, + }, + }), + }); +} diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js index a33c6ae8030..be61831fc4d 100644 --- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js @@ -46,6 +46,11 @@ export default class IssuableBulkUpdateSidebar { // https://gitlab.com/gitlab-org/gitlab/-/issues/325874 issuableEventHub.$on('issuables:enableBulkEdit', () => this.toggleBulkEdit(null, true)); issuableEventHub.$on('issuables:updateBulkEdit', () => this.updateFormState()); + + // These events are connected to the logic inside `move_issues_button.vue`, + // so that only one action can be performed at a time + issuableEventHub.$on('issuables:bulkMoveStarted', () => this.toggleSubmitButtonDisabled(true)); + issuableEventHub.$on('issuables:bulkMoveEnded', () => this.updateFormState()); } initDropdowns() { @@ -89,6 +94,8 @@ export default class IssuableBulkUpdateSidebar { this.updateSelectedIssuableIds(); IssuableBulkUpdateActions.setOriginalDropdownData(); + + issuableEventHub.$emit('issuables:selectionChanged', !noCheckedIssues); } prepForSubmit() { diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue index acb6aa93f0f..a110ba658f7 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -565,6 +565,7 @@ export default { bulkUpdateSidebar.initBulkUpdateSidebar('issuable_'); bulkUpdateSidebar.initStatusDropdown(); bulkUpdateSidebar.initSubscriptionsDropdown(); + bulkUpdateSidebar.initMoveIssuesButton(); const usersSelect = await import('~/users_select'); const UsersSelect = usersSelect.default; diff --git a/app/assets/javascripts/issues/list/graphql.js b/app/assets/javascripts/issues/list/graphql.js new file mode 100644 index 00000000000..5ef61727a3d --- /dev/null +++ b/app/assets/javascripts/issues/list/graphql.js @@ -0,0 +1,25 @@ +import produce from 'immer'; +import createDefaultClient from '~/lib/graphql'; +import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; + +const resolvers = { + Mutation: { + reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => { + const variables = JSON.parse(serializedVariables); + const sourceData = cache.readQuery({ query: getIssuesQuery, variables }); + + const data = produce(sourceData, (draftData) => { + const issues = draftData[namespace].issues.nodes.slice(); + const issueToMove = issues[oldIndex]; + issues.splice(oldIndex, 1); + issues.splice(newIndex, 0, issueToMove); + + draftData[namespace].issues.nodes = issues; + }); + + cache.writeQuery({ query: getIssuesQuery, variables, data }); + }, + }, +}; + +export const gqlClient = createDefaultClient(resolvers); diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js index 93333c31b34..569ca006af5 100644 --- a/app/assets/javascripts/issues/list/index.js +++ b/app/assets/javascripts/issues/list/index.js @@ -1,12 +1,11 @@ -import produce from 'immer'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; -import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue'; import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue'; +import { gqlClient } from './graphql'; export function mountJiraIssuesListApp() { const el = document.querySelector('.js-jira-issues-import-status'); @@ -56,26 +55,6 @@ export function mountIssuesListApp() { Vue.use(VueApollo); Vue.use(VueRouter); - const resolvers = { - Mutation: { - reorderIssues: (_, { oldIndex, newIndex, namespace, serializedVariables }, { cache }) => { - const variables = JSON.parse(serializedVariables); - const sourceData = cache.readQuery({ query: getIssuesQuery, variables }); - - const data = produce(sourceData, (draftData) => { - const issues = draftData[namespace].issues.nodes.slice(); - const issueToMove = issues[oldIndex]; - issues.splice(oldIndex, 1); - issues.splice(newIndex, 0, issueToMove); - - draftData[namespace].issues.nodes = issues; - }); - - cache.writeQuery({ query: getIssuesQuery, variables, data }); - }, - }, - }; - const { autocompleteAwardEmojisPath, calendarPath, @@ -125,7 +104,7 @@ export function mountIssuesListApp() { el, name: 'IssuesListRoot', apolloProvider: new VueApollo({ - defaultClient: createDefaultClient(resolvers), + defaultClient: gqlClient, }), router: new VueRouter({ base: window.location.pathname, diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue index 4941f22230b..ed5466ff99c 100644 --- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue +++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue @@ -219,7 +219,6 @@ export default { :empty-message="$options.i18n.empty.merge" :keep-component-mounted="false" :is-empty="isEmpty" - :is-invalid="isInvalid" :is-unavailable="isLintUnavailable" :title="$options.i18n.tabMergedYaml" lazy diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue index 318940478a8..ebfc7d312b4 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue @@ -45,18 +45,17 @@ export default { }; }, update({ project: { branchRules } }) { - this.branchProtection = branchRules.nodes.find( - (rule) => rule.name === this.branch, - )?.branchProtection; + const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch); + this.branchProtection = branchRule?.branchProtection; + this.approvalRules = branchRule?.approvalRules; }, }, }, data() { return { branch: getParameterByName(BRANCH_PARAM_NAME), - branchProtection: { - approvalRules: {}, - }, + branchProtection: {}, + approvalRules: {}, }; }, computed: { @@ -104,7 +103,7 @@ export default { : this.$options.i18n.branchNameOrPattern; }, approvals() { - return this.branchProtection?.approvalRules?.nodes || []; + return this.approvalRules?.nodes || []; }, }, methods: { diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue index 28a1c09fa82..12de136a21a 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/protection_row.vue @@ -64,10 +64,10 @@ export default { <template> <div - class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4" + class="gl-display-flex gl-align-items-center gl-border-gray-100 gl-mb-4 gl-pt-4 gl-border-t-1" :class="{ 'gl-border-t-solid': showDivider }" > - <div class="gl-display-flex gl-w-half gl-justify-content-space-between"> + <div class="gl-display-flex gl-w-half gl-justify-content-space-between gl-align-items-center"> <div class="gl-mr-7 gl-w-quarter">{{ title }}</div> <gl-avatars-inline diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql index 3ac165498a1..4ca474a5ceb 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql +++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql @@ -44,6 +44,23 @@ query getBranchRulesDetails($projectPath: ID!) { } } } + approvalRules { + nodes { + id + name + type + approvalsRequired + eligibleApprovers { + nodes { + id + name + username + webUrl + avatarUrl + } + } + } + } } } } diff --git a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue index 55420c9c732..886db07a901 100644 --- a/app/assets/javascripts/projects/settings/components/transfer_project_form.vue +++ b/app/assets/javascripts/projects/settings/components/transfer_project_form.vue @@ -1,30 +1,14 @@ <script> -import { GlFormGroup, GlAlert } from '@gitlab/ui'; -import { debounce } from 'lodash'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; -import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import TransferLocations from '~/groups_projects/components/transfer_locations.vue'; import { getTransferLocations } from '~/api/projects_api'; -import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; -import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; -import { s__, __ } from '~/locale'; -import currentUserNamespace from '../graphql/queries/current_user_namespace.query.graphql'; export default { name: 'TransferProjectForm', components: { - GlFormGroup, - NamespaceSelect, + TransferLocations, ConfirmDanger, - GlAlert, }, - i18n: { - errorMessage: s__( - 'ProjectTransfer|An error occurred fetching the transfer locations, please refresh the page and try again.', - ), - alertDismissAlert: __('Dismiss'), - }, - inject: ['projectId'], props: { confirmationPhrase: { type: String, @@ -37,146 +21,32 @@ export default { }, data() { return { - userNamespaces: [], - groupNamespaces: [], - initialNamespacesLoaded: false, - selectedNamespace: null, - hasError: false, - isLoading: false, - isSearchLoading: false, - searchTerm: '', - page: 1, - totalPages: 1, + selectedTransferLocation: null, }; }, + computed: { hasSelectedNamespace() { - return Boolean(this.selectedNamespace?.id); + return Boolean(this.selectedTransferLocation?.id); }, - hasNextPageOfGroups() { - return this.page < this.totalPages; + }, + watch: { + selectedTransferLocation(selectedTransferLocation) { + this.$emit('selectTransferLocation', selectedTransferLocation.id); }, }, methods: { - async handleShow() { - if (this.initialNamespacesLoaded) { - return; - } - - this.isLoading = true; - - [this.groupNamespaces, this.userNamespaces] = await Promise.all([ - this.getGroupNamespaces(), - this.getUserNamespaces(), - ]); - - this.isLoading = false; - this.initialNamespacesLoaded = true; - }, - handleSelect(selectedNamespace) { - this.selectedNamespace = selectedNamespace; - this.$emit('selectNamespace', selectedNamespace.id); - }, - async getGroupNamespaces() { - try { - const { data: groupNamespaces, headers } = await getTransferLocations(this.projectId, { - page: this.page, - search: this.searchTerm, - }); - - const { totalPages } = parseIntPagination(normalizeHeaders(headers)); - this.totalPages = totalPages; - - return groupNamespaces.map(({ id, full_name: humanName }) => ({ - id, - humanName, - })); - } catch (error) { - this.hasError = true; - - return []; - } - }, - async getUserNamespaces() { - try { - const { - data: { - currentUser: { namespace }, - }, - } = await this.$apollo.query({ - query: currentUserNamespace, - }); - - if (!namespace) { - return []; - } - - return [ - { - id: getIdFromGraphQLId(namespace.id), - humanName: namespace.fullName, - }, - ]; - } catch (error) { - this.hasError = true; - - return []; - } - }, - async handleLoadMoreGroups() { - this.isLoading = true; - this.page += 1; - - const groupNamespaces = await this.getGroupNamespaces(); - this.groupNamespaces.push(...groupNamespaces); - - this.isLoading = false; - }, - debouncedSearch: debounce(async function debouncedSearch() { - this.isSearchLoading = true; - - this.groupNamespaces = await this.getGroupNamespaces(); - - this.isSearchLoading = false; - }, DEBOUNCE_DELAY), - handleSearch(searchTerm) { - this.searchTerm = searchTerm; - this.page = 1; - - this.debouncedSearch(); - }, - handleAlertDismiss() { - this.hasError = false; - }, + getTransferLocations, }, }; </script> <template> <div> - <gl-alert - v-if="hasError" - variant="danger" - :dismiss-label="$options.i18n.alertDismissLabel" - @dismiss="handleAlertDismiss" - >{{ $options.i18n.errorMessage }}</gl-alert - > - <gl-form-group> - <namespace-select - data-testid="transfer-project-namespace" - :full-width="true" - :group-namespaces="groupNamespaces" - :user-namespaces="userNamespaces" - :selected-namespace="selectedNamespace" - :has-next-page-of-groups="hasNextPageOfGroups" - :is-loading="isLoading" - :is-search-loading="isSearchLoading" - :should-filter-namespaces="false" - @select="handleSelect" - @load-more-groups="handleLoadMoreGroups" - @search="handleSearch" - @show="handleShow" - /> - </gl-form-group> + <transfer-locations + v-model="selectedTransferLocation" + data-testid="transfer-project-namespace" + :group-transfer-locations-api-method="getTransferLocations" + /> <confirm-danger :disabled="!hasSelectedNamespace" :phrase="confirmationPhrase" diff --git a/app/assets/javascripts/projects/settings/init_transfer_project_form.js b/app/assets/javascripts/projects/settings/init_transfer_project_form.js index 89c158a9ba8..7f810e647ae 100644 --- a/app/assets/javascripts/projects/settings/init_transfer_project_form.js +++ b/app/assets/javascripts/projects/settings/init_transfer_project_form.js @@ -12,7 +12,7 @@ export default () => { Vue.use(VueApollo); const { - projectId, + projectId: resourceId, targetFormId = null, targetHiddenInputId = null, buttonText: confirmButtonText = '', @@ -27,7 +27,7 @@ export default () => { }), provide: { confirmDangerMessage, - projectId, + resourceId, }, render(createElement) { return createElement(TransferProjectForm, { @@ -36,7 +36,7 @@ export default () => { confirmationPhrase, }, on: { - selectNamespace: (id) => { + selectTransferLocation: (id) => { if (targetHiddenInputId && document.getElementById(targetHiddenInputId)) { document.getElementById(targetHiddenInputId).value = id; } diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue index 0f5560ff628..02323e5a0c6 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue @@ -43,6 +43,11 @@ export default { required: false, default: false, }, + disabled: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -128,7 +133,7 @@ export default { </script> <template> - <div class="block js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown"> + <div class="js-issuable-move-block issuable-move-dropdown sidebar-move-issue-dropdown"> <div v-gl-tooltip.left.viewport data-testid="move-collapsed" @@ -141,7 +146,7 @@ export default { <gl-dropdown ref="dropdown" :block="true" - :disabled="moveInProgress" + :disabled="moveInProgress || disabled" class="hide-collapsed" toggle-class="js-sidebar-dropdown-toggle" @shown="fetchProjects" diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue index 7e735f358eb..9fbf8042784 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue @@ -62,6 +62,9 @@ export default { issuableId() { return getIdFromGraphQLId(this.issuable.id); }, + issuableIid() { + return this.issuable.iid; + }, createdInPastDay() { const createdSecondsAgo = differenceInSeconds(new Date(this.issuable.createdAt), new Date()); return createdSecondsAgo < SECONDS_IN_DAY; @@ -193,6 +196,8 @@ export default { class="issue-check gl-mr-0" :checked="checked" :data-id="issuableId" + :data-iid="issuableIid" + :data-type="issuable.type" @input="$emit('checked-input', $event)" > <span class="gl-sr-only">{{ issuable.title }}</span> diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue index bc10f84b819..0318dd22bfa 100644 --- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue +++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue @@ -7,6 +7,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import issuableEventHub from '~/issues/list/eventhub'; import { DEFAULT_SKELETON_COUNT, PAGE_SIZE_STORAGE_KEY } from '../constants'; import IssuableBulkEditSidebar from './issuable_bulk_edit_sidebar.vue'; import IssuableItem from './issuable_item.vue'; @@ -266,6 +267,7 @@ export default { handleIssuableCheckedInput(issuable, value) { this.checkedIssuables[this.issuableId(issuable)].checked = value; this.$emit('update-legacy-bulk-edit'); + issuableEventHub.$emit('issuables:issuableChecked', issuable, value); }, handleAllIssuablesCheckedInput(value) { Object.keys(this.checkedIssuables).forEach((issuableId) => { diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb index f3283c88740..89e8a261288 100644 --- a/app/controllers/projects/alerting/notifications_controller.rb +++ b/app/controllers/projects/alerting/notifications_controller.rb @@ -18,8 +18,9 @@ module Projects def create token = extract_alert_manager_token(request) result = notify_service.execute(token, integration) + has_something_to_return = result.success? && result.http_status != :created - if result.success? + if has_something_to_return render json: AlertManagement::AlertSerializer.new.represent(result.payload[:alerts]), code: result.http_status else head result.http_status diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 418e7233e21..f9a11ddb1db 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -38,9 +38,6 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options_hash) unfoldable_positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user).unfoldable - diffs.unfold_diff_files(unfoldable_positions) - diffs.write_cache - options = { merge_request: @merge_request, commit: commit, @@ -63,7 +60,16 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic options[:allow_tree_conflicts] ] - return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs]) + if Feature.enabled?(:check_etags_diffs_batch_before_write_cache, merge_request.project) && !stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs]) + return + end + + diffs.unfold_diff_files(unfoldable_positions) + diffs.write_cache + + if Feature.disabled?(:check_etags_diffs_batch_before_write_cache, merge_request.project) && !stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs]) + return + end render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options) end diff --git a/app/controllers/projects/prometheus/alerts_controller.rb b/app/controllers/projects/prometheus/alerts_controller.rb index c3dc17694d9..27ac64e5758 100644 --- a/app/controllers/projects/prometheus/alerts_controller.rb +++ b/app/controllers/projects/prometheus/alerts_controller.rb @@ -23,11 +23,7 @@ module Projects token = extract_alert_manager_token(request) result = notify_service.execute(token) - if result.success? - render json: AlertManagement::AlertSerializer.new.represent(result.payload[:alerts]), code: result.http_status - else - head result.http_status - end + head result.http_status end private @@ -37,19 +33,6 @@ module Projects .new(project, params.permit!) end - def serialize_as_json(alert_obj) - serializer.represent(alert_obj) - end - - def serializer - PrometheusAlertSerializer - .new(project: project, current_user: current_user) - end - - def alerts - alerts_finder.execute - end - def alert @alert ||= alerts_finder(metric: params[:id]).execute.first || render_404 end diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb index b4235a77867..51457d443a1 100644 --- a/app/finders/license_template_finder.rb +++ b/app/finders/license_template_finder.rb @@ -34,9 +34,13 @@ class LicenseTemplateFinder private + def available_licenses + Licensee::License.all(featured: popular_only?) + end + def vendored_licenses strong_memoize(:vendored_licenses) do - Licensee::License.all(featured: popular_only?).map do |license| + available_licenses.map do |license| LicenseTemplate.new( key: license.key, name: license.name, diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 8cc600fc68e..c98cfed7493 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -100,8 +100,7 @@ module Types field :detailed_merge_status, ::Types::MergeRequests::DetailedMergeStatusEnum, null: true, calls_gitaly: true, - description: 'Detailed merge status of the merge request.', - alpha: { milestone: '15.3' } + description: 'Detailed merge status of the merge request.' field :mergeable_discussions_state, GraphQL::Types::Boolean, null: true, calls_gitaly: true, diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 361b1a8dca9..a1bef44a815 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -636,6 +636,10 @@ class ApplicationSetting < ApplicationRecord addressable_url: { allow_localhost: true, allow_local_network: false }, allow_blank: true + validates :product_analytics_enabled, + presence: true, + allow_blank: true + attr_encrypted :asset_proxy_secret_key, mode: :per_attribute_iv, key: Settings.attr_encrypted_db_key_base_truncated, diff --git a/app/models/diff_viewer/server_side.rb b/app/models/diff_viewer/server_side.rb index a1defb2594f..fb127de2bc7 100644 --- a/app/models/diff_viewer/server_side.rb +++ b/app/models/diff_viewer/server_side.rb @@ -9,14 +9,6 @@ module DiffViewer self.size_limit = 5.megabytes end - def prepare! - return if Feature.enabled?(:disable_load_entire_blob_for_diff_viewer, diff_file.repository.project) - - # TODO: remove this after resolving #342703 - diff_file.old_blob&.load_all_data! - diff_file.new_blob&.load_all_data! - end - def render_error # Files that are not stored in the repository, like LFS files and # build artifacts, can only be rendered using a client-side viewer, diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 2300ec2996d..3710abc46ea 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -1,50 +1,18 @@ # frozen_string_literal: true class Experiment < ApplicationRecord - has_many :experiment_users has_many :experiment_subjects, inverse_of: :experiment validates :name, presence: true, uniqueness: true, length: { maximum: 255 } - def self.add_user(name, group_type, user, context = {}) - by_name(name).record_user_and_group(user, group_type, context) - end - - def self.add_group(name, variant:, group:) - add_subject(name, variant: variant, subject: group) - end - def self.add_subject(name, variant:, subject:) by_name(name).record_subject_and_variant!(subject, variant) end - def self.record_conversion_event(name, user, context = {}) - by_name(name).record_conversion_event_for_user(user, context) - end - def self.by_name(name) find_or_create_by!(name: name) end - # Create or update the recorded experiment_user row for the user in this experiment. - def record_user_and_group(user, group_type, context = {}) - experiment_user = experiment_users.find_or_initialize_by(user: user) - experiment_user.assign_attributes(group_type: group_type, context: merged_context(experiment_user, context)) - # We only call save when necessary because this causes the request to stick to the primary DB - # even when the save is a no-op - # https://gitlab.com/gitlab-org/gitlab/-/issues/324649 - experiment_user.save! if experiment_user.changed? - - experiment_user - end - - def record_conversion_event_for_user(user, context = {}) - experiment_user = experiment_users.find_by(user: user) - return unless experiment_user - - experiment_user.update!(converted_at: Time.current, context: merged_context(experiment_user, context)) - end - def record_conversion_event_for_subject(subject, context = {}) raise 'Incompatible subject provided!' unless ExperimentSubject.valid_subject?(subject) diff --git a/app/models/experiment_user.rb b/app/models/experiment_user.rb deleted file mode 100644 index e447becc1bd..00000000000 --- a/app/models/experiment_user.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -class ExperimentUser < ApplicationRecord - include ::Gitlab::Experimentation::GroupTypes - - belongs_to :experiment - belongs_to :user - - enum group_type: { GROUP_CONTROL => 0, GROUP_EXPERIMENTAL => 1 } - - validates :experiment_id, presence: true - validates :user_id, presence: true - validates :group_type, presence: true -end diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb index ab0fdbd777f..d4867600853 100644 --- a/app/models/integrations/datadog.rb +++ b/app/models/integrations/datadog.rb @@ -91,7 +91,7 @@ module Integrations with_options if: :activated? do validates :api_key, presence: true, format: { with: /\A\w+\z/ } - validates :datadog_site, format: { with: /\A[\w\.]+\z/, allow_blank: true } + validates :datadog_site, format: { with: %r{\A\w+([-.]\w+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?\z}, allow_blank: true } validates :api_url, public_url: { allow_blank: true } validates :datadog_site, presence: true, unless: -> (obj) { obj.api_url.present? } validates :api_url, presence: true, unless: -> (obj) { obj.datadog_site.present? } diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb index 29e1ba88528..62e2e75db2a 100644 --- a/app/models/ml/candidate.rb +++ b/app/models/ml/candidate.rb @@ -14,6 +14,10 @@ module Ml default_value_for(:iid) { SecureRandom.uuid } + def artifact_root + "/ml_candidate_#{iid}/-/" + end + class << self def with_project_id_and_iid(project_id, iid) return unless project_id.present? && iid.present? diff --git a/app/services/bulk_imports/create_pipeline_trackers_service.rb b/app/services/bulk_imports/create_pipeline_trackers_service.rb index f5b944e6df5..7fa62e0ce8a 100644 --- a/app/services/bulk_imports/create_pipeline_trackers_service.rb +++ b/app/services/bulk_imports/create_pipeline_trackers_service.rb @@ -55,6 +55,8 @@ module BulkImports message: 'Pipeline skipped as source instance version not compatible with pipeline', bulk_import_entity_id: entity.id, bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline[:pipeline], minimum_source_version: minimum_version, maximum_source_version: maximum_version, diff --git a/app/services/concerns/alert_management/responses.rb b/app/services/concerns/alert_management/responses.rb index 183a831a00a..e48d07d26c0 100644 --- a/app/services/concerns/alert_management/responses.rb +++ b/app/services/concerns/alert_management/responses.rb @@ -7,6 +7,10 @@ module AlertManagement ServiceResponse.success(payload: { alerts: Array(alerts) }) end + def created + ServiceResponse.success(http_status: :created) + end + def bad_request ServiceResponse.error(message: 'Bad Request', http_status: :bad_request) end diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb index 9f260345937..1e084c0e5eb 100644 --- a/app/services/projects/prometheus/alerts/notify_service.rb +++ b/app/services/projects/prometheus/alerts/notify_service.rb @@ -36,9 +36,9 @@ module Projects truncate_alerts! if max_alerts_exceeded? - alert_responses = process_prometheus_alerts + process_prometheus_alerts - alert_response(alert_responses) + created end def self.processable?(payload) @@ -152,12 +152,6 @@ module Projects .execute end end - - def alert_response(alert_responses) - alerts = alert_responses.flat_map { |resp| resp.payload[:alerts] }.compact - - success(alerts) - end end end end diff --git a/app/views/projects/_transfer.html.haml b/app/views/projects/_transfer.html.haml index e3aa2d8afc9..108d340ec4d 100644 --- a/app/views/projects/_transfer.html.haml +++ b/app/views/projects/_transfer.html.haml @@ -20,5 +20,4 @@ %li= _('You will need to update your local repositories to point to the new location.') %li= _('Project visibility level will be changed to match namespace rules when transferring to a group.') = hidden_field_tag(hidden_input_id) - = label_tag :new_namespace_id, _('Select a new namespace'), class: 'gl-font-weight-bold' .js-transfer-project-form{ data: initial_data } diff --git a/app/views/projects/protected_tags/shared/_dropdown.html.haml b/app/views/projects/protected_tags/shared/_dropdown.html.haml index 9c7f532fa29..9d5d649bc40 100644 --- a/app/views/projects/protected_tags/shared/_dropdown.html.haml +++ b/app/views/projects/protected_tags/shared/_dropdown.html.haml @@ -1,8 +1,8 @@ = f.hidden_field(:name) -= dropdown_tag('Select tag or create wildcard', += dropdown_tag(s_('ProtectedBranch|Select tag or create wildcard'), options: { toggle_class: 'js-protected-tag-select js-filter-submit wide monospace', - filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: "Search protected tags", + filter: true, dropdown_class: "dropdown-menu-selectable capitalize-header git-revision-dropdown", placeholder: s_("ProtectedBranch|Search protected tags"), footer_content: true, data: { show_no: true, show_any: true, show_upcoming: true, selected: params[:protected_tag_name], @@ -10,6 +10,6 @@ %ul.dropdown-footer-list %li - %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: "New Protected Tag" } - Create wildcard + %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item", title: s_("ProtectedBranch|New Protected Tag") } + = s_('ProtectedBranch|Create wildcard') %code diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml index 5f3ea281278..0a85a353e27 100644 --- a/app/views/projects/protected_tags/shared/_tags_list.html.haml +++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml @@ -1,9 +1,9 @@ .protected-tags-list.js-protected-tags-list - if @protected_tags.empty? .card-header - Protected tags (0) + = s_('ProtectedBranch|Protected tags (%{tags_count})') % { tags_count: 0 } %p.settings-message.text-center - No tags are protected. + = s_('ProtectedBranch|No tags are protected.') - else - can_admin_project = can?(current_user, :admin_project, @project) @@ -16,9 +16,12 @@ %col %thead %tr - %th Protected tags (#{@protected_tags_count}) - %th Last commit - %th Allowed to create + %th + = s_('ProtectedBranch|Protected tags (%{tags_count})') % { tags_count: @protected_tags_count } + %th + = s_('ProtectedBranch|Last commit') + %th + = s_('ProtectedBranch|Allowed to create') - if can_admin_project %th %tbody diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml index e6bdefc64d2..beb5b527669 100644 --- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml +++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml @@ -1,5 +1,6 @@ - type = local_assigns.fetch(:type) - is_issue = type == :issues +- move_data = { projects_fetch_path: autocomplete_projects_path(project_id: @project.id), project_full_path: @project.full_path } %aside.issues-bulk-update.js-right-sidebar.right-sidebar{ "aria-live" => "polite", data: { 'signed-in': current_user.present? }, 'aria-label': _('Bulk update') } .issuable-sidebar.hidden @@ -42,6 +43,9 @@ .title = _('Subscriptions') .js-subscriptions-dropdown + - if is_issue + .block + .js-move-issues{ data: move_data } = hidden_field_tag "update[issuable_ids]", [] = hidden_field_tag :state_event, params[:state_event] diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb index ada3210624c..d23d57c33ab 100644 --- a/app/workers/bulk_imports/entity_worker.rb +++ b/app/workers/bulk_imports/entity_worker.rb @@ -12,13 +12,18 @@ module BulkImports worker_has_external_dependencies! def perform(entity_id, current_stage = nil) + @entity = ::BulkImports::Entity.find(entity_id) + if stage_running?(entity_id, current_stage) logger.info( structured_payload( bulk_import_entity_id: entity_id, - bulk_import_id: bulk_import_id(entity_id), + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, current_stage: current_stage, message: 'Stage running', + source_version: source_version, importer: 'gitlab_migration' ) ) @@ -29,9 +34,12 @@ module BulkImports logger.info( structured_payload( bulk_import_entity_id: entity_id, - bulk_import_id: bulk_import_id(entity_id), + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, current_stage: current_stage, message: 'Stage starting', + source_version: source_version, importer: 'gitlab_migration' ) ) @@ -44,23 +52,34 @@ module BulkImports ) end rescue StandardError => e - logger.error( - structured_payload( + log_exception(e, + { bulk_import_entity_id: entity_id, - bulk_import_id: bulk_import_id(entity_id), + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, current_stage: current_stage, - message: e.message, + message: 'Entity failed', + source_version: source_version, importer: 'gitlab_migration' - ) + } ) Gitlab::ErrorTracking.track_exception( - e, bulk_import_entity_id: entity_id, bulk_import_id: bulk_import_id(entity_id), importer: 'gitlab_migration' + e, + bulk_import_entity_id: entity_id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + source_version: source_version, + importer: 'gitlab_migration' ) end private + attr_reader :entity + def stage_running?(entity_id, stage) return unless stage @@ -71,12 +90,18 @@ module BulkImports BulkImports::Tracker.next_pipeline_trackers_for(entity_id).update(status_event: 'enqueue') end - def bulk_import_id(entity_id) - @bulk_import_id ||= Entity.find(entity_id).bulk_import_id + def source_version + entity.bulk_import.source_version_info.to_s end def logger @logger ||= Gitlab::Import::Logger.build end + + def log_exception(exception, payload) + Gitlab::ExceptionLogFormatter.format!(exception, payload) + + logger.error(structured_payload(payload)) + end end end diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb index a57071ddcf1..1a5f6250429 100644 --- a/app/workers/bulk_imports/export_request_worker.rb +++ b/app/workers/bulk_imports/export_request_worker.rb @@ -22,7 +22,19 @@ module BulkImports if e.retriable?(entity) retry_request(e, entity) else - log_export_failure(e, entity) + log_exception(e, + { + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + message: "Request to export #{entity.source_type} failed", + source_version: entity.bulk_import.source_version_info.to_s, + importer: 'gitlab_migration' + } + ) + + BulkImports::Failure.create(failure_attributes(e, entity)) entity.fail_op! end @@ -41,22 +53,7 @@ module BulkImports ) end - def log_export_failure(exception, entity) - Gitlab::Import::Logger.error( - structured_payload( - log_attributes(exception, entity).merge( - bulk_import_id: entity.bulk_import_id, - bulk_import_entity_type: entity.source_type, - message: "Request to export #{entity.source_type} failed", - importer: 'gitlab_migration' - ) - ) - ) - - BulkImports::Failure.create(log_attributes(exception, entity)) - end - - def log_attributes(exception, entity) + def failure_attributes(exception, entity) { bulk_import_entity_id: entity.id, pipeline_class: 'ExportRequestWorker', @@ -84,15 +81,16 @@ module BulkImports ::GlobalID.parse(response.dig(*query.data_path, 'id')).model_id rescue StandardError => e - Gitlab::Import::Logger.error( - structured_payload( - log_attributes(e, entity).merge( - message: 'Failed to fetch source entity id', - bulk_import_id: entity.bulk_import_id, - bulk_import_entity_type: entity.source_type, - importer: 'gitlab_migration' - ) - ) + log_exception(e, + { + message: 'Failed to fetch source entity id', + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + source_version: entity.bulk_import.source_version_info.to_s, + importer: 'gitlab_migration' + } ) nil @@ -107,18 +105,29 @@ module BulkImports end def retry_request(exception, entity) - Gitlab::Import::Logger.error( - structured_payload( - log_attributes(exception, entity).merge( - message: 'Retrying export request', - bulk_import_id: entity.bulk_import_id, - bulk_import_entity_type: entity.source_type, - importer: 'gitlab_migration' - ) - ) + log_exception(exception, + { + message: 'Retrying export request', + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + source_version: entity.bulk_import.source_version_info.to_s, + importer: 'gitlab_migration' + } ) self.class.perform_in(2.seconds, entity.id) end + + def logger + @logger ||= Gitlab::Import::Logger.build + end + + def log_exception(exception, payload) + Gitlab::ExceptionLogFormatter.format!(exception, payload) + + logger.error(structured_payload(payload)) + end end end diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb index 6d314774cff..5716f6e3f31 100644 --- a/app/workers/bulk_imports/pipeline_worker.rb +++ b/app/workers/bulk_imports/pipeline_worker.rb @@ -17,24 +17,34 @@ module BulkImports .find_by_id(pipeline_tracker_id) if pipeline_tracker.present? + @entity = @pipeline_tracker.entity + logger.info( structured_payload( - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, message: 'Pipeline starting', + source_version: source_version, importer: 'gitlab_migration' ) ) run else + @entity = ::BulkImports::Entity.find(entity_id) + logger.error( structured_payload( bulk_import_entity_id: entity_id, - bulk_import_id: bulk_import_id(entity_id), + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_tracker_id: pipeline_tracker_id, message: 'Unstarted pipeline not found', + source_version: source_version, importer: 'gitlab_migration' ) ) @@ -46,10 +56,10 @@ module BulkImports private - attr_reader :pipeline_tracker + attr_reader :pipeline_tracker, :entity def run - return skip_tracker if pipeline_tracker.entity.failed? + return skip_tracker if entity.failed? raise(Pipeline::ExpiredError, 'Pipeline timeout') if job_timeout? raise(Pipeline::FailedError, "Export from source instance failed: #{export_status.error}") if export_failed? @@ -65,33 +75,39 @@ module BulkImports fail_tracker(e) end - def bulk_import_id(entity_id) - @bulk_import_id ||= Entity.find(entity_id).bulk_import_id + def source_version + entity.bulk_import.source_version_info.to_s end def fail_tracker(exception) pipeline_tracker.update!(status_event: 'fail_op', jid: jid) - logger.error( - structured_payload( - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + log_exception(exception, + { + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, - message: exception.message, + message: 'Pipeline failed', + source_version: source_version, importer: 'gitlab_migration' - ) + } ) Gitlab::ErrorTracking.track_exception( exception, - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, + source_version: source_version, importer: 'gitlab_migration' ) BulkImports::Failure.create( - bulk_import_entity_id: context.entity.id, + bulk_import_entity_id: entity.id, pipeline_class: pipeline_tracker.pipeline_name, pipeline_step: 'pipeline_worker_run', exception_class: exception.class.to_s, @@ -109,7 +125,7 @@ module BulkImports delay, pipeline_tracker.id, pipeline_tracker.stage, - pipeline_tracker.entity.id + entity.id ) end @@ -128,7 +144,7 @@ module BulkImports def job_timeout? return false unless file_extraction_pipeline? - (Time.zone.now - pipeline_tracker.entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT + (Time.zone.now - entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT end def export_failed? @@ -150,14 +166,17 @@ module BulkImports end def retry_tracker(exception) - logger.error( - structured_payload( - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + log_exception(exception, + { + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, - message: "Retrying error: #{exception.message}", + message: "Retrying pipeline", + source_version: source_version, importer: 'gitlab_migration' - ) + } ) pipeline_tracker.update!(status_event: 'retry', jid: jid) @@ -168,15 +187,23 @@ module BulkImports def skip_tracker logger.info( structured_payload( - bulk_import_entity_id: pipeline_tracker.entity.id, - bulk_import_id: pipeline_tracker.entity.bulk_import_id, + bulk_import_entity_id: entity.id, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, message: 'Skipping pipeline due to failed entity', + source_version: source_version, importer: 'gitlab_migration' ) ) pipeline_tracker.update!(status_event: 'skip', jid: jid) end + + def log_exception(exception, payload) + Gitlab::ExceptionLogFormatter.format!(exception, payload) + logger.error(structured_payload(payload)) + end end end diff --git a/config/feature_flags/development/disable_load_entire_blob_for_diff_viewer.yml b/config/feature_flags/development/check_etags_diffs_batch_before_write_cache.yml index 5e767e3540b..fb03ff91d0a 100644 --- a/config/feature_flags/development/disable_load_entire_blob_for_diff_viewer.yml +++ b/config/feature_flags/development/check_etags_diffs_batch_before_write_cache.yml @@ -1,8 +1,8 @@ --- -name: disable_load_entire_blob_for_diff_viewer -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/99029 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/376330 -milestone: '15.5' +name: check_etags_diffs_batch_before_write_cache +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101421 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/378333 +milestone: '15.6' type: development -group: group::source code +group: group::code review default_enabled: false diff --git a/db/migrate/20221010103207_add_product_analytics_enabled_to_application_settings.rb b/db/migrate/20221010103207_add_product_analytics_enabled_to_application_settings.rb new file mode 100644 index 00000000000..24887e7b9fb --- /dev/null +++ b/db/migrate/20221010103207_add_product_analytics_enabled_to_application_settings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddProductAnalyticsEnabledToApplicationSettings < Gitlab::Database::Migration[2.0] + def change + add_column :application_settings, :product_analytics_enabled, :boolean, default: false, null: false + end +end diff --git a/db/schema_migrations/20221010103207 b/db/schema_migrations/20221010103207 new file mode 100644 index 00000000000..24fcfc34c41 --- /dev/null +++ b/db/schema_migrations/20221010103207 @@ -0,0 +1 @@ +04997da3ff51b8be05fd765c6534f92a15eea0a4ee4a535f1cb84c6da4e1bdd5
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c99b4488148..4ee8368d8ad 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -11491,6 +11491,7 @@ CREATE TABLE application_settings ( password_expiration_enabled boolean DEFAULT false NOT NULL, password_expires_in_days integer DEFAULT 90 NOT NULL, password_expires_notice_before_days integer DEFAULT 7 NOT NULL, + product_analytics_enabled boolean DEFAULT false NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)), CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)), diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index fca488f9021..1a765f0ed43 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -14210,7 +14210,7 @@ Represents the merge access level of a branch protection. | <a id="mergerequestdefaultsquashcommitmessage"></a>`defaultSquashCommitMessage` | [`String`](#string) | Default squash commit message of the merge request. | | <a id="mergerequestdescription"></a>`description` | [`String`](#string) | Description of the merge request (Markdown rendered as HTML for caching). | | <a id="mergerequestdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. | -| <a id="mergerequestdetailedmergestatus"></a>`detailedMergeStatus` **{warning-solid}** | [`DetailedMergeStatus`](#detailedmergestatus) | **Introduced** in 15.3. This feature is in Alpha. It can be changed or removed at any time. Detailed merge status of the merge request. | +| <a id="mergerequestdetailedmergestatus"></a>`detailedMergeStatus` | [`DetailedMergeStatus`](#detailedmergestatus) | Detailed merge status of the merge request. | | <a id="mergerequestdiffheadsha"></a>`diffHeadSha` | [`String`](#string) | Diff head SHA of the merge request. | | <a id="mergerequestdiffrefs"></a>`diffRefs` | [`DiffRefs`](#diffrefs) | References of the base SHA, the head SHA, and the start SHA for this merge request. | | <a id="mergerequestdiffstatssummary"></a>`diffStatsSummary` | [`DiffStatsSummary`](#diffstatssummary) | Summary of which files were changed in this merge request. | diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index 01e42fda2c9..1244a9510c1 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -209,7 +209,15 @@ We keep track of retried tests in the `$RETRIED_TESTS_REPORT_FILE` file saved as See the [experiment issue](https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1148). -### Single database testing +### Compatibility testing + +By default, we run all tests with the versions that runs on GitLab.com. + +Other versions (usually one back-compatible version, and one forward-compatible version) should be running in nightly scheduled pipelines. + +Exceptions to this general guideline should be motivated and documented. + +#### Single database testing By default, all tests run with [multiple databases](database/multiple_databases.md). diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index 213c615326f..c77ecbd802c 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -340,6 +340,29 @@ To move an issue: ### Bulk move issues **(FREE SELF)** +#### From the issues list + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15991) in GitLab 15.5. + +You can move multiple issues at the same time when you’re in a project. +You can't move tasks or test cases. + +Prerequisite: + +- You must have at least the Reporter role for the project. + +To move multiple issues at the same time: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Issues**. +1. Select **Edit issues**. A sidebar on the right of your screen appears. +1. Select the checkboxes next to each issue you want to move. +1. From the right sidebar, select **Move selected**. +1. From the dropdown list, select the destination project. +1. Select **Move**. + +#### From the Rails console + You can move all open issues from one project to another. Prerequisites: diff --git a/lib/api/entities/ml/mlflow/run.rb b/lib/api/entities/ml/mlflow/run.rb index a8e1cfe08dd..8b16c67611f 100644 --- a/lib/api/entities/ml/mlflow/run.rb +++ b/lib/api/entities/ml/mlflow/run.rb @@ -6,7 +6,7 @@ module API module Mlflow class Run < Grape::Entity expose :run do - expose(:info) { |candidate| RunInfo.represent(candidate) } + expose :itself, using: RunInfo, as: :info expose :data do expose :metrics, using: Metric expose :params, using: RunParam diff --git a/lib/api/entities/ml/mlflow/run_info.rb b/lib/api/entities/ml/mlflow/run_info.rb index 096950e349d..d3934545ba4 100644 --- a/lib/api/entities/ml/mlflow/run_info.rb +++ b/lib/api/entities/ml/mlflow/run_info.rb @@ -11,7 +11,7 @@ module API expose(:start_time) { |candidate| candidate.start_time || 0 } expose :end_time, expose_nil: false expose(:status) { |candidate| candidate.status.to_s.upcase } - expose(:artifact_uri) { |candidate| 'not_implemented' } + expose(:artifact_uri) { |candidate, options| "#{options[:packages_url]}#{candidate.artifact_root}" } expose(:lifecycle_stage) { |candidate| 'active' } expose(:user_id) { |candidate| candidate.user_id.to_s } diff --git a/lib/api/entities/ml/mlflow/update_run.rb b/lib/api/entities/ml/mlflow/update_run.rb index 090d69b8895..55def810ef5 100644 --- a/lib/api/entities/ml/mlflow/update_run.rb +++ b/lib/api/entities/ml/mlflow/update_run.rb @@ -5,13 +5,7 @@ module API module Ml module Mlflow class UpdateRun < Grape::Entity - expose :run_info - - private - - def run_info - RunInfo.represent object - end + expose :itself, using: RunInfo, as: :run_info end end end diff --git a/lib/api/ml/mlflow.rb b/lib/api/ml/mlflow.rb index 2ffb04ebcbd..f3195e5b6c5 100644 --- a/lib/api/ml/mlflow.rb +++ b/lib/api/ml/mlflow.rb @@ -68,6 +68,15 @@ module API def find_candidate!(iid) candidate_repository.by_iid(iid) || resource_not_found! end + + def packages_url + path = api_v4_projects_packages_generic_package_version_path( + id: user_project.id, package_name: '', file_name: '' + ) + path = path.delete_suffix('/package_version') + + "#{request.base_url}#{path}" + end end params do @@ -143,7 +152,8 @@ module API optional :tags, type: Array, desc: 'This will be ignored' end post 'create', urgency: :low do - present candidate_repository.create!(experiment, params[:start_time]), with: Entities::Ml::Mlflow::Run + present candidate_repository.create!(experiment, params[:start_time]), + with: Entities::Ml::Mlflow::Run, packages_url: packages_url end desc 'Gets an MLFlow Run, which maps to GitLab Candidates' do @@ -155,7 +165,7 @@ module API optional :run_uuid, type: String, desc: 'This parameter is ignored' end get 'get', urgency: :low do - present candidate, with: Entities::Ml::Mlflow::Run + present candidate, with: Entities::Ml::Mlflow::Run, packages_url: packages_url end desc 'Updates a Run.' do @@ -174,7 +184,7 @@ module API post 'update', urgency: :low do candidate_repository.update(candidate, params[:status], params[:end_time]) - present candidate, with: Entities::Ml::Mlflow::UpdateRun + present candidate, with: Entities::Ml::Mlflow::UpdateRun, packages_url: packages_url end desc 'Logs a metric to a run.' do diff --git a/lib/bulk_imports/common/pipelines/entity_finisher.rb b/lib/bulk_imports/common/pipelines/entity_finisher.rb index 5066f622d57..a52504d04bc 100644 --- a/lib/bulk_imports/common/pipelines/entity_finisher.rb +++ b/lib/bulk_imports/common/pipelines/entity_finisher.rb @@ -24,11 +24,13 @@ module BulkImports end logger.info( - bulk_import_id: context.bulk_import_id, - bulk_import_entity_id: context.entity.id, - bulk_import_entity_type: context.entity.source_type, + bulk_import_id: entity.bulk_import_id, + bulk_import_entity_id: entity.id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_class: self.class.name, message: "Entity #{entity.status_name}", + source_version: entity.bulk_import.source_version_info.to_s, importer: 'gitlab_migration' ) diff --git a/lib/bulk_imports/pipeline/runner.rb b/lib/bulk_imports/pipeline/runner.rb index ef9575d1e96..81f8dee30d9 100644 --- a/lib/bulk_imports/pipeline/runner.rb +++ b/lib/bulk_imports/pipeline/runner.rb @@ -99,7 +99,7 @@ module BulkImports end def log_import_failure(exception, step) - attributes = { + failure_attributes = { bulk_import_entity_id: context.entity.id, pipeline_class: pipeline, pipeline_step: step, @@ -108,16 +108,18 @@ module BulkImports correlation_id_value: Labkit::Correlation::CorrelationId.current_or_new_id } - error( - bulk_import_id: context.bulk_import_id, - pipeline_step: step, - exception_class: exception.class.to_s, - exception_message: exception.message, - message: "Pipeline failed", - importer: 'gitlab_migration' + log_exception( + exception, + log_params( + { + bulk_import_id: context.bulk_import_id, + pipeline_step: step, + message: 'Pipeline failed' + } + ) ) - BulkImports::Failure.create(attributes) + BulkImports::Failure.create(failure_attributes) end def info(extra = {}) @@ -128,17 +130,15 @@ module BulkImports logger.warn(log_params(extra)) end - def error(extra = {}) - logger.error(log_params(extra)) - end - def log_params(extra) defaults = { bulk_import_id: context.bulk_import_id, bulk_import_entity_id: context.entity.id, bulk_import_entity_type: context.entity.source_type, + source_full_path: context.entity.source_full_path, pipeline_class: pipeline, context_extra: context.extra, + source_version: context.entity.bulk_import.source_version_info.to_s, importer: 'gitlab_migration' } @@ -150,6 +150,19 @@ module BulkImports def logger @logger ||= Gitlab::Import::Logger.build end + + def log_exception(exception, payload) + Gitlab::ExceptionLogFormatter.format!(exception, payload) + logger.error(structured_payload(payload)) + end + + def structured_payload(payload = {}) + context = Gitlab::ApplicationContext.current.merge( + 'class' => self.class.name + ) + + payload.stringify_keys.merge(context) + end end end end diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb index 924de132840..ae55dae1201 100644 --- a/lib/gitlab/diff/file_collection/base.rb +++ b/lib/gitlab/diff/file_collection/base.rb @@ -46,7 +46,9 @@ module Gitlab # This is either the new path, otherwise the old path for the diff_file def diff_file_paths - diff_files.map(&:file_path) + diffs.map do |diff| + diff.new_path.presence || diff.old_path + end end # This is both the new and old paths for the diff_file diff --git a/lib/gitlab/kas/client.rb b/lib/gitlab/kas/client.rb index 753a185344e..768810d5545 100644 --- a/lib/gitlab/kas/client.rb +++ b/lib/gitlab/kas/client.rb @@ -64,7 +64,7 @@ module Gitlab def credentials if URI(Gitlab::Kas.internal_url).scheme == 'grpcs' - GRPC::Core::ChannelCredentials.new + GRPC::Core::ChannelCredentials.new(::Gitlab::X509::Certificate.ca_certs_bundle) else :this_channel_is_insecure end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 48c36dfc24b..2974061a549 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22673,6 +22673,21 @@ msgstr "" msgid "IssuesAnalytics|Total:" msgstr "" +msgid "Issues|Move selected" +msgstr "" + +msgid "Issues|Tasks and test cases can not be moved." +msgstr "" + +msgid "Issues|Tasks can not be moved." +msgstr "" + +msgid "Issues|Test cases can not be moved." +msgstr "" + +msgid "Issues|There was an error while moving the issues." +msgstr "" + msgid "Issue|Title" msgstr "" @@ -32717,6 +32732,9 @@ msgstr "" msgid "ProtectedBranch|Allow all users with push access to force push." msgstr "" +msgid "ProtectedBranch|Allowed to create" +msgstr "" + msgid "ProtectedBranch|Allowed to force push" msgstr "" @@ -32753,15 +32771,27 @@ msgstr "" msgid "ProtectedBranch|Code owner approval" msgstr "" +msgid "ProtectedBranch|Create wildcard" +msgstr "" + msgid "ProtectedBranch|Does not apply to users allowed to push. Optional sections are not enforced." msgstr "" msgid "ProtectedBranch|Keep stable branches secure and force developers to use merge requests." msgstr "" +msgid "ProtectedBranch|Last commit" +msgstr "" + msgid "ProtectedBranch|Learn more." msgstr "" +msgid "ProtectedBranch|New Protected Tag" +msgstr "" + +msgid "ProtectedBranch|No tags are protected." +msgstr "" + msgid "ProtectedBranch|Protect" msgstr "" @@ -32777,12 +32807,21 @@ msgstr "" msgid "ProtectedBranch|Protected branches, merge request approvals, and status checks will appear here once configured." msgstr "" +msgid "ProtectedBranch|Protected tags (%{tags_count})" +msgstr "" + msgid "ProtectedBranch|Reject code pushes that change files listed in the CODEOWNERS file." msgstr "" msgid "ProtectedBranch|Require approval from code owners:" msgstr "" +msgid "ProtectedBranch|Search protected tags" +msgstr "" + +msgid "ProtectedBranch|Select tag or create wildcard" +msgstr "" + msgid "ProtectedBranch|There are currently no protected branches, protect a branch with the form above." msgstr "" diff --git a/qa/Gemfile b/qa/Gemfile index 56552044561..35384fbbef3 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -19,7 +19,7 @@ gem 'knapsack', '~> 4.0' gem 'parallel_tests', '~> 3.13' gem 'rotp', '~> 6.2.0' gem 'timecop', '~> 0.9.5' -gem 'parallel', '~> 1.19' +gem 'parallel', '~> 1.22', '>= 1.22.1' gem 'rainbow', '~> 3.1.1' gem 'rspec-parameterized', '~> 0.5.2' gem 'octokit', '~> 5.6.1' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index a07d960f32e..da7238fbc53 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -187,7 +187,7 @@ GEM sawyer (~> 0.9) oj (3.13.21) os (1.1.4) - parallel (1.19.2) + parallel (1.22.1) parallel_tests (3.13.0) parallel parser (3.1.2.1) @@ -319,7 +319,7 @@ DEPENDENCIES knapsack (~> 4.0) nokogiri (~> 1.13, >= 1.13.9) octokit (~> 5.6.1) - parallel (~> 1.19) + parallel (~> 1.22, >= 1.22.1) parallel_tests (~> 3.13) pry-byebug (~> 3.10.1) rainbow (~> 3.1.1) diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb index bef15b46fcd..c7cb97341d1 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb @@ -36,6 +36,15 @@ module QA end end + let(:invalid_content) do + <<~YAML + + job3: + stage: stage_foo + script: echo 'Done.' + YAML + end + before do # Make sure a pipeline is created before visiting pipeline editor page. # Otherwise, test might timeout before the page finishing fetching pipeline status. @@ -80,7 +89,7 @@ module QA invalid_msg = 'syntax is invalid' Page::Project::PipelineEditor::Show.perform do |show| - show.write_to_editor(SecureRandom.hex(10)) + show.write_to_editor(invalid_content) aggregate_failures do show.go_to_visualize_tab @@ -90,8 +99,10 @@ module QA show.simulate_pipeline expect(show.tab_alert_title).to have_content('Pipeline simulation completed with errors') + expect(show.ci_syntax_validate_message).to have_content('CI configuration is invalid') + show.go_to_view_merged_yaml_tab - expect(show.tab_alert_message).to have_content(invalid_msg) + expect(show).to have_source_editor expect(show.ci_syntax_validate_message).to have_content('CI configuration is invalid') end diff --git a/rubocop/cop/gitlab/json.rb b/rubocop/cop/gitlab/json.rb index 56846e3c276..3510882fd6d 100644 --- a/rubocop/cop/gitlab/json.rb +++ b/rubocop/cop/gitlab/json.rb @@ -7,25 +7,43 @@ module RuboCop extend RuboCop::Cop::AutoCorrector MSG = <<~EOL - Avoid calling `JSON` directly. Instead, use the `Gitlab::Json` - wrapper. This allows us to alter the JSON parser being used. + Prefer `Gitlab::Json` over calling `JSON` or `to_json` directly. See https://docs.gitlab.com/ee/development/json.html EOL def_node_matcher :json_node?, <<~PATTERN - (send (const {nil? | (const nil? :ActiveSupport)} :JSON)...) + (send (const {nil? | (const nil? :ActiveSupport)} :JSON) $_ $...) + PATTERN + + def_node_matcher :to_json_call?, <<~PATTERN + (send $_ :to_json) PATTERN def on_send(node) - return unless json_node?(node) + method_name, arg_source = match_node(node) + return unless method_name add_offense(node) do |corrector| - _, method_name, *arg_nodes = *node - - replacement = "Gitlab::Json.#{method_name}(#{arg_nodes.map(&:source).join(', ')})" + replacement = "Gitlab::Json.#{method_name}(#{arg_source})" corrector.replace(node.source_range, replacement) end end + + private + + def match_node(node) + method_name, arg_nodes = json_node?(node) + + # Only match if the method is implemented by Gitlab::Json + if method_name && ::Gitlab::Json.methods(false).include?(method_name) + return [method_name, arg_nodes.map(&:source).join(', ')] + end + + receiver = to_json_call?(node) + return [:generate, receiver.source] if receiver + + nil + end end end end diff --git a/scripts/lint_templates_bash.rb b/scripts/lint_templates_bash.rb index 8db9469ecdf..cd36bb629ab 100755 --- a/scripts/lint_templates_bash.rb +++ b/scripts/lint_templates_bash.rb @@ -58,6 +58,12 @@ module LintTemplatesBash def check_template(template) parsed = process_content(template.content) + + unless parsed.valid? + warn "#{template.full_name} is invalid: #{parsed.errors.inspect}" + return true + end + results = parsed.jobs.map do |name, job| out, success = check_job(job) diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml index 91c645a0ed9..ae1fe527100 100644 --- a/scripts/review_apps/base-config.yaml +++ b/scripts/review_apps/base-config.yaml @@ -22,11 +22,11 @@ gitlab: gitaly: resources: requests: - cpu: 2400m - memory: 1000M + cpu: 1200m + memory: 600M limits: - cpu: 3600m - memory: 1500M + cpu: 1800m + memory: 1000M persistence: size: 10G storageClass: ssd diff --git a/spec/controllers/projects/alerting/notifications_controller_spec.rb b/spec/controllers/projects/alerting/notifications_controller_spec.rb index b3feeb7c07b..5ce2950f95f 100644 --- a/spec/controllers/projects/alerting/notifications_controller_spec.rb +++ b/spec/controllers/projects/alerting/notifications_controller_spec.rb @@ -16,9 +16,6 @@ RSpec.describe Projects::Alerting::NotificationsController do end shared_examples 'process alert payload' do |notify_service_class| - let(:alert_1) { build(:alert_management_alert, project: project) } - let(:alert_2) { build(:alert_management_alert, project: project) } - let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) } let(:notify_service) { instance_double(notify_service_class, execute: service_response) } before do @@ -35,11 +32,14 @@ RSpec.describe Projects::Alerting::NotificationsController do it 'responds with the alert data' do make_request - expect(json_response).to contain_exactly( - { 'iid' => alert_1.iid, 'title' => alert_1.title }, - { 'iid' => alert_2.iid, 'title' => alert_2.title } - ) - expect(response).to have_gitlab_http_status(:ok) + if service_response.payload.present? + expect(json_response).to contain_exactly( + { 'iid' => alert_1.iid, 'title' => alert_1.title }, + { 'iid' => alert_2.iid, 'title' => alert_2.title } + ) + end + + expect(response).to have_gitlab_http_status(service_response.http_status) end it 'does not pass excluded parameters to the notify service' do @@ -146,6 +146,9 @@ RSpec.describe Projects::Alerting::NotificationsController do context 'with generic alert payload' do it_behaves_like 'process alert payload', Projects::Alerting::NotifyService do + let(:alert_1) { build(:alert_management_alert, project: project) } + let(:alert_2) { build(:alert_management_alert, project: project) } + let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) } let(:payload) { { title: 'Alert title' } } end end @@ -154,6 +157,7 @@ RSpec.describe Projects::Alerting::NotificationsController do include PrometheusHelpers it_behaves_like 'process alert payload', Projects::Prometheus::Alerts::NotifyService do + let(:service_response) { ServiceResponse.success(http_status: :created) } let(:payload) { prometheus_alert_payload } end end diff --git a/spec/controllers/projects/prometheus/alerts_controller_spec.rb b/spec/controllers/projects/prometheus/alerts_controller_spec.rb index 2c2c8180143..09b9f25c0c6 100644 --- a/spec/controllers/projects/prometheus/alerts_controller_spec.rb +++ b/spec/controllers/projects/prometheus/alerts_controller_spec.rb @@ -56,7 +56,7 @@ RSpec.describe Projects::Prometheus::AlertsController do describe 'POST #notify' do let(:alert_1) { build(:alert_management_alert, :prometheus, project: project) } let(:alert_2) { build(:alert_management_alert, :prometheus, project: project) } - let(:service_response) { ServiceResponse.success(payload: { alerts: [alert_1, alert_2] }) } + let(:service_response) { ServiceResponse.success(http_status: :created) } let(:notify_service) { instance_double(Projects::Prometheus::Alerts::NotifyService, execute: service_response) } before do @@ -68,17 +68,12 @@ RSpec.describe Projects::Prometheus::AlertsController do .and_return(notify_service) end - it 'returns ok if notification succeeds' do + it 'returns created if notification succeeds' do expect(notify_service).to receive(:execute).and_return(service_response) post :notify, params: project_params, session: { as: :json } - expect(json_response).to contain_exactly( - { 'iid' => alert_1.iid, 'title' => alert_1.title }, - { 'iid' => alert_2.iid, 'title' => alert_2.title } - ) - - expect(response).to have_gitlab_http_status(:ok) + expect(response).to have_gitlab_http_status(:created) end it 'returns unprocessable entity if notification fails' do diff --git a/spec/factories/experiment_users.rb b/spec/factories/experiment_users.rb deleted file mode 100644 index 66c39d684eb..00000000000 --- a/spec/factories/experiment_users.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :experiment_user do - experiment - user - group_type { :control } - converted_at { nil } - end -end diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb index 47b28b88108..a2dea7f048b 100644 --- a/spec/features/issues/user_interacts_with_awards_spec.rb +++ b/spec/features/issues/user_interacts_with_awards_spec.rb @@ -209,22 +209,25 @@ RSpec.describe 'User interacts with awards' do it 'adds award to issue' do first('[data-testid="award-button"]').click - + wait_for_requests expect(page).to have_selector('[data-testid="award-button"].selected') expect(first('[data-testid="award-button"]')).to have_content '1' visit project_issue_path(project, issue) + wait_for_requests expect(first('[data-testid="award-button"]')).to have_content '1' end it 'removes award from issue', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/375241' do first('[data-testid="award-button"]').click + wait_for_requests find('[data-testid="award-button"].selected').click - + wait_for_requests expect(first('[data-testid="award-button"]')).to have_content '0' visit project_issue_path(project, issue) + wait_for_requests expect(first('[data-testid="award-button"]')).to have_content '0' end diff --git a/spec/fixtures/api/schemas/ml/run.json b/spec/fixtures/api/schemas/ml/run.json index 2418f44b21f..48d0ed25ce4 100644 --- a/spec/fixtures/api/schemas/ml/run.json +++ b/spec/fixtures/api/schemas/ml/run.json @@ -27,15 +27,43 @@ "end_time" ], "properties": { - "run_id": { "type": "string" }, - "run_uuid": { "type": "string" }, - "experiment_id": { "type": "string" }, - "artifact_location": { "type": "string" }, - "start_time": { "type": "integer" }, - "end_time": { "type": "integer" }, + "run_id": { + "type": "string" + }, + "run_uuid": { + "type": "string" + }, + "experiment_id": { + "type": "string" + }, + "artifact_uri": { + "type": "string" + }, + "start_time": { + "type": "integer" + }, + "end_time": { + "type": "integer" + }, "user_id": "", - "status": { "type": { "enum" : ["RUNNING", "SCHEDULED", "FINISHED", "FAILED", "KILLED"] } }, - "lifecycle_stage": { "type": { "enum" : ["active"] } } + "status": { + "type": { + "enum": [ + "RUNNING", + "SCHEDULED", + "FINISHED", + "FAILED", + "KILLED" + ] + } + }, + "lifecycle_stage": { + "type": { + "enum": [ + "active" + ] + } + } } }, "data": { @@ -44,4 +72,4 @@ } } } -} +}
\ No newline at end of file diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js new file mode 100644 index 00000000000..c3308caf4b0 --- /dev/null +++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js @@ -0,0 +1,283 @@ +import { + GlDropdown, + GlDropdownItem, + GlAlert, + GlSearchBoxByType, + GlIntersectionObserver, + GlLoadingIcon, +} from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json'; +import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json'; +import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import TransferLocations from '~/groups_projects/components/transfer_locations.vue'; +import { getTransferLocations } from '~/api/projects_api'; +import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql'; + +jest.mock('~/api/projects_api', () => ({ + getTransferLocations: jest.fn(), +})); + +describe('TransferLocations', () => { + let wrapper; + + // Default data + const resourceId = '1'; + const defaultPropsData = { + groupTransferLocationsApiMethod: getTransferLocations, + value: null, + }; + + // Mock requests + const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse); + const mockResolvedGetTransferLocations = ({ + data = transferLocationsResponsePage1, + page = '1', + nextPage = '2', + total = '4', + totalPages = '2', + prevPage = null, + } = {}) => { + getTransferLocations.mockResolvedValueOnce({ + data, + headers: { + 'x-per-page': '2', + 'x-page': page, + 'x-total': total, + 'x-total-pages': totalPages, + 'x-next-page': nextPage, + 'x-prev-page': prevPage, + }, + }); + }; + const mockRejectedGetTransferLocations = () => { + const error = new Error(); + + getTransferLocations.mockRejectedValueOnce(error); + }; + + // VTU wrapper helpers + Vue.use(VueApollo); + const createComponent = ({ + propsData = {}, + requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]], + } = {}) => { + wrapper = mountExtended(TransferLocations, { + provide: { + resourceId, + }, + propsData: { + ...defaultPropsData, + ...propsData, + }, + apolloProvider: createMockApollo(requestHandlers), + }); + }; + + const findDropdown = () => wrapper.findComponent(GlDropdown); + const showDropdown = async () => { + findDropdown().vm.$emit('show'); + await waitForPromises(); + }; + const findUserTransferLocations = () => + wrapper + .findByTestId('user-transfer-locations') + .findAllComponents(GlDropdownItem) + .wrappers.map((dropdownItem) => dropdownItem.text()); + const findGroupTransferLocations = () => + wrapper + .findByTestId('group-transfer-locations') + .findAllComponents(GlDropdownItem) + .wrappers.map((dropdownItem) => dropdownItem.text()); + const findAlert = () => wrapper.findComponent(GlAlert); + const findSearch = () => wrapper.findComponent(GlSearchBoxByType); + const searchEmitInput = () => findSearch().vm.$emit('input', 'foo'); + const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); + const intersectionObserverEmitAppear = () => findIntersectionObserver().vm.$emit('appear'); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when `GlDropdown` is opened', () => { + it('fetches and renders user and group transfer locations', async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showDropdown(); + + const { namespace } = currentUserNamespaceQueryResponse.data.currentUser; + + expect(findUserTransferLocations()).toEqual([namespace.fullName]); + expect(findGroupTransferLocations()).toEqual( + transferLocationsResponsePage1.map((transferLocation) => transferLocation.full_name), + ); + }); + + describe('when transfer locations have already been fetched', () => { + beforeEach(async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showDropdown(); + }); + + it('does not fetch transfer locations', async () => { + getTransferLocations.mockClear(); + defaultQueryHandler.mockClear(); + + await showDropdown(); + + expect(getTransferLocations).not.toHaveBeenCalled(); + expect(defaultQueryHandler).not.toHaveBeenCalled(); + }); + }); + + describe('when `getTransferLocations` API call fails', () => { + it('displays dismissible error alert', async () => { + mockRejectedGetTransferLocations(); + createComponent(); + await showDropdown(); + + const alert = findAlert(); + + expect(alert.exists()).toBe(true); + + alert.vm.$emit('dismiss'); + await nextTick(); + + expect(alert.exists()).toBe(false); + }); + }); + + describe('when `currentUser` GraphQL query fails', () => { + it('displays error alert', async () => { + mockResolvedGetTransferLocations(); + const error = new Error(); + createComponent({ + requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]], + }); + await showDropdown(); + + expect(findAlert().exists()).toBe(true); + }); + }); + }); + + describe('when transfer location is selected', () => { + it('displays transfer location as selected', () => { + const [{ id, full_name: humanName }] = transferLocationsResponsePage1; + + createComponent({ + propsData: { + value: { + id, + humanName, + }, + }, + }); + + expect(findDropdown().props('text')).toBe(humanName); + }); + }); + + describe('when search is typed in', () => { + const transferLocationsResponseSearch = [transferLocationsResponsePage1[0]]; + + const arrange = async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showDropdown(); + mockResolvedGetTransferLocations({ data: transferLocationsResponseSearch }); + searchEmitInput(); + await nextTick(); + }; + + it('sets `isSearchLoading` prop to `true`', async () => { + await arrange(); + + expect(findSearch().props('isLoading')).toBe(true); + }); + + it('passes `search` param to API call and updates group transfer locations', async () => { + await arrange(); + + await waitForPromises(); + + expect(getTransferLocations).toHaveBeenCalledWith( + resourceId, + expect.objectContaining({ search: 'foo' }), + ); + expect(findGroupTransferLocations()).toEqual( + transferLocationsResponseSearch.map((transferLocation) => transferLocation.full_name), + ); + }); + }); + + describe('when there are no more pages', () => { + it('does not show intersection observer', async () => { + mockResolvedGetTransferLocations({ + data: transferLocationsResponsePage1, + nextPage: null, + total: '2', + totalPages: '1', + prevPage: null, + }); + createComponent(); + await showDropdown(); + + expect(findIntersectionObserver().exists()).toBe(false); + }); + }); + + describe('when intersection observer appears', () => { + const arrange = async () => { + mockResolvedGetTransferLocations(); + createComponent(); + await showDropdown(); + + mockResolvedGetTransferLocations({ + data: transferLocationsResponsePage2, + page: '2', + nextPage: null, + prevPage: '1', + totalPages: '2', + }); + + intersectionObserverEmitAppear(); + await nextTick(); + }; + + it('shows loading icon', async () => { + await arrange(); + + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('passes `page` param to API call', async () => { + await arrange(); + + await waitForPromises(); + + expect(getTransferLocations).toHaveBeenCalledWith( + resourceId, + expect.objectContaining({ page: 2 }), + ); + }); + + it('updates dropdown with new group transfer locations', async () => { + await arrange(); + + await waitForPromises(); + + expect(findGroupTransferLocations()).toEqual( + [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map( + ({ full_name: fullName }) => fullName, + ), + ); + }); + }); +}); diff --git a/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js new file mode 100644 index 00000000000..c432d722637 --- /dev/null +++ b/spec/frontend/issuable/bulk_update_sidebar/components/move_issues_button_spec.js @@ -0,0 +1,554 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import { cloneDeep } from 'lodash'; +import VueApollo from 'vue-apollo'; +import { GlAlert } from '@gitlab/ui'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; +import createFlash from '~/flash'; +import { logError } from '~/lib/logger'; +import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue'; +import MoveIssuesButton from '~/issuable/bulk_update_sidebar/components/move_issues_button.vue'; +import issuableEventHub from '~/issues/list/eventhub'; +import moveIssueMutation from '~/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql'; +import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql'; +import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql'; +import { getIssuesCountsQueryResponse, getIssuesQueryResponse } from 'jest/issues/list/mock_data'; +import { + WORK_ITEM_TYPE_ENUM_ISSUE, + WORK_ITEM_TYPE_ENUM_INCIDENT, + WORK_ITEM_TYPE_ENUM_TASK, + WORK_ITEM_TYPE_ENUM_TEST_CASE, +} from '~/work_items/constants'; + +jest.mock('~/flash'); +jest.mock('~/lib/logger'); +useMockLocationHelper(); + +const mockDefaultProps = { + projectFullPath: 'flight/FlightJS', + projectsFetchPath: '/-/autocomplete/projects?project_id=1', +}; + +const mockDestinationProject = { + full_path: 'gitlab-org/GitLabTest', +}; + +const mockMutationErrorMessage = 'Example error message'; + +const mockIssue = { + iid: '15', + type: WORK_ITEM_TYPE_ENUM_ISSUE, +}; + +const mockIncident = { + iid: '32', + type: WORK_ITEM_TYPE_ENUM_INCIDENT, +}; + +const mockTask = { + iid: '40', + type: WORK_ITEM_TYPE_ENUM_TASK, +}; + +const mockTestCase = { + iid: '51', + type: WORK_ITEM_TYPE_ENUM_TEST_CASE, +}; + +const selectedIssuesMocks = { + tasksOnly: [mockTask], + testCasesOnly: [mockTestCase], + issuesOnly: [mockIssue, mockIncident], + tasksAndTestCases: [mockTask, mockTestCase], + issuesAndTasks: [mockIssue, mockIncident, mockTask], + issuesAndTestCases: [mockIssue, mockIncident, mockTestCase], + issuesTasksAndTestCases: [mockIssue, mockIncident, mockTask, mockTestCase], +}; + +let getIssuesQueryCompleteResponse = getIssuesQueryResponse; +if (IS_EE) { + getIssuesQueryCompleteResponse = cloneDeep(getIssuesQueryResponse); + getIssuesQueryCompleteResponse.data.project.issues.nodes[0].blockingCount = 1; + getIssuesQueryCompleteResponse.data.project.issues.nodes[0].healthStatus = null; + getIssuesQueryCompleteResponse.data.project.issues.nodes[0].weight = 5; +} + +const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({ + data: { + issueMove: { + errors: [], + }, + }, +}); + +const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({ + data: { + issueMove: { + errors: [{ message: mockMutationErrorMessage }], + }, + }, +}); + +const rejectedMutationMock = jest.fn().mockRejectedValue({}); + +const mockIssuesQueryResponse = jest.fn().mockResolvedValue(getIssuesQueryCompleteResponse); +const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse); + +describe('MoveIssuesButton', () => { + Vue.use(VueApollo); + + let wrapper; + let fakeApollo; + + const findAlert = () => wrapper.findComponent(GlAlert); + const findDropdown = () => wrapper.findComponent(IssuableMoveDropdown); + const emitMoveIssuablesEvent = () => { + findDropdown().vm.$emit('move-issuable', mockDestinationProject); + }; + + const createComponent = (data = {}, mutationResolverMock = rejectedMutationMock) => { + fakeApollo = createMockApollo([ + [moveIssueMutation, mutationResolverMock], + [getIssuesQuery, mockIssuesQueryResponse], + [getIssuesCountsQuery, mockIssuesCountsQueryResponse], + ]); + + fakeApollo.defaultClient.cache.writeQuery({ + query: getIssuesQuery, + variables: { + isProject: true, + fullPath: mockDefaultProps.projectFullPath, + }, + data: getIssuesQueryCompleteResponse.data, + }); + + fakeApollo.defaultClient.cache.writeQuery({ + query: getIssuesCountsQuery, + variables: { + isProject: true, + }, + data: getIssuesCountsQueryResponse.data, + }); + + wrapper = shallowMount(MoveIssuesButton, { + data() { + return { + ...data, + }; + }, + propsData: { + ...mockDefaultProps, + }, + apolloProvider: fakeApollo, + }); + }; + + beforeEach(() => { + // Needed due to a bug in Apollo: https://github.com/apollographql/apollo-client/issues/8900 + // eslint-disable-next-line no-console + console.warn = jest.fn(); + }); + + afterEach(() => { + wrapper.destroy(); + fakeApollo = null; + }); + + describe('`Move selected` dropdown', () => { + it('renders disabled by default', () => { + createComponent(); + expect(findDropdown().exists()).toBe(true); + expect(findDropdown().attributes('disabled')).toBe('true'); + }); + + it.each` + selectedIssuablesMock | disabled | status | testMessage + ${[]} | ${true} | ${'disabled'} | ${'nothing is selected'} + ${selectedIssuesMocks.tasksOnly} | ${true} | ${'disabled'} | ${'only tasks are selected'} + ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'disabled'} | ${'only test cases are selected'} + ${selectedIssuesMocks.issuesOnly} | ${false} | ${'enabled'} | ${'only issues are selected'} + ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'disabled'} | ${'tasks and test cases are selected'} + ${selectedIssuesMocks.issuesAndTasks} | ${false} | ${'enabled'} | ${'issues and tasks are selected'} + ${selectedIssuesMocks.issuesAndTestCases} | ${false} | ${'enabled'} | ${'issues and test cases are selected'} + ${selectedIssuesMocks.issuesTasksAndTestCases} | ${false} | ${'enabled'} | ${'issues and tasks and test cases are selected'} + `('renders $status if $testMessage', async ({ selectedIssuablesMock, disabled }) => { + createComponent({ selectedIssuables: selectedIssuablesMock }); + + await nextTick(); + + if (disabled) { + expect(findDropdown().attributes('disabled')).toBe('true'); + } else { + expect(findDropdown().attributes('disabled')).toBeUndefined(); + } + }); + }); + + describe('warning message', () => { + it.each` + selectedIssuablesMock | warningExists | visibility | message | testMessage + ${[]} | ${false} | ${'not visible'} | ${'empty'} | ${'nothing is selected'} + ${selectedIssuesMocks.tasksOnly} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'only tasks are selected'} + ${selectedIssuesMocks.testCasesOnly} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'only test cases are selected'} + ${selectedIssuesMocks.issuesOnly} | ${false} | ${'not visible'} | ${'empty'} | ${'only issues are selected'} + ${selectedIssuesMocks.tasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'tasks and test cases are selected'} + ${selectedIssuesMocks.issuesAndTasks} | ${true} | ${'visible'} | ${'Tasks can not be moved.'} | ${'issues and tasks are selected'} + ${selectedIssuesMocks.issuesAndTestCases} | ${true} | ${'visible'} | ${'Test cases can not be moved.'} | ${'issues and test cases are selected'} + ${selectedIssuesMocks.issuesTasksAndTestCases} | ${true} | ${'visible'} | ${'Tasks and test cases can not be moved.'} | ${'issues and tasks and test cases are selected'} + `( + 'is $visibility with `$message` message if $testMessage', + async ({ selectedIssuablesMock, warningExists, message }) => { + createComponent({ selectedIssuables: selectedIssuablesMock }); + + await nextTick(); + + const alert = findAlert(); + expect(alert.exists()).toBe(warningExists); + + if (warningExists) { + expect(alert.text()).toBe(message); + expect(alert.attributes('variant')).toBe('warning'); + } + }, + ); + }); + + describe('moveIssues method', () => { + describe('changes the `Move selected` dropdown loading state', () => { + it('keeps loading state to false when no issue is selected', async () => { + createComponent(); + emitMoveIssuablesEvent(); + + await nextTick(); + + expect(findDropdown().props('moveInProgress')).toBe(false); + }); + + it('keeps loading state to false when only tasks are selected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly }); + emitMoveIssuablesEvent(); + + await nextTick(); + + expect(findDropdown().props('moveInProgress')).toBe(false); + }); + + it('keeps loading state to false when only test cases are selected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly }); + emitMoveIssuablesEvent(); + + await nextTick(); + + expect(findDropdown().props('moveInProgress')).toBe(false); + }); + + it('keeps loading state to false when only tasks and test cases are selected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases }); + emitMoveIssuablesEvent(); + + await nextTick(); + + expect(findDropdown().props('moveInProgress')).toBe(false); + }); + + it('sets loading state to true when issues are moving', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }); + emitMoveIssuablesEvent(); + + await nextTick(); + + expect(findDropdown().props('moveInProgress')).toBe(true); + }); + + it('sets loading state to false when all mutations succeed', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }, + resolvedMutationWithoutErrorsMock, + ); + emitMoveIssuablesEvent(); + + await nextTick(); + await waitForPromises(); + + expect(findDropdown().props('moveInProgress')).toBe(false); + }); + + it('sets loading state to false when a mutation returns errors', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }, + resolvedMutationWithErrorsMock, + ); + emitMoveIssuablesEvent(); + + await nextTick(); + await waitForPromises(); + + expect(findDropdown().props('moveInProgress')).toBe(false); + }); + + it('sets loading state to false when a mutation is rejected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }); + emitMoveIssuablesEvent(); + + await nextTick(); + await waitForPromises(); + + expect(findDropdown().props('moveInProgress')).toBe(false); + }); + }); + + describe('handles events', () => { + beforeEach(() => { + jest.spyOn(issuableEventHub, '$emit'); + }); + + it('does not emit any event when no issue is selected', async () => { + createComponent(); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(issuableEventHub.$emit).not.toHaveBeenCalled(); + }); + + it('does not emit any event when only tasks are selected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly }); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(issuableEventHub.$emit).not.toHaveBeenCalled(); + }); + + it('does not emit any event when only test cases are selected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly }); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(issuableEventHub.$emit).not.toHaveBeenCalled(); + }); + + it('does not emit any event when only tasks and test cases are selected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases }); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(issuableEventHub.$emit).not.toHaveBeenCalled(); + }); + + it('emits `issuables:bulkMoveStarted` when issues are moving', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }); + emitMoveIssuablesEvent(); + + expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveStarted'); + }); + + it('emits `issuables:bulkMoveEnded` when all mutations succeed', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }, + resolvedMutationWithoutErrorsMock, + ); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded'); + }); + + it('emits `issuables:bulkMoveEnded` when a mutation returns errors', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }, + resolvedMutationWithErrorsMock, + ); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded'); + }); + + it('emits `issuables:bulkMoveEnded` when a mutation is rejected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(issuableEventHub.$emit).toHaveBeenCalledWith('issuables:bulkMoveEnded'); + }); + }); + + describe('shows errors', () => { + it('does not create flashes or logs errors when no issue is selected', async () => { + createComponent(); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(logError).not.toHaveBeenCalled(); + expect(createFlash).not.toHaveBeenCalled(); + }); + + it('does not create flashes or logs errors when only tasks are selected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.tasksOnly }); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(logError).not.toHaveBeenCalled(); + expect(createFlash).not.toHaveBeenCalled(); + }); + + it('does not create flashes or logs errors when only test cases are selected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.testCasesOnly }); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(logError).not.toHaveBeenCalled(); + expect(createFlash).not.toHaveBeenCalled(); + }); + + it('does not create flashes or logs errors when only tasks and test cases are selected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.tasksAndTestCases }); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(logError).not.toHaveBeenCalled(); + expect(createFlash).not.toHaveBeenCalled(); + }); + + it('does not create flashes or logs errors when issues are moved without errors', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }, + resolvedMutationWithoutErrorsMock, + ); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(logError).not.toHaveBeenCalled(); + expect(createFlash).not.toHaveBeenCalled(); + }); + + it('creates a flash and logs errors when a mutation returns errors', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }, + resolvedMutationWithErrorsMock, + ); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + // We're mocking two issues so it will log two errors + expect(logError).toHaveBeenCalledTimes(2); + expect(logError).toHaveBeenNthCalledWith( + 1, + `Error moving issue. Error message: ${mockMutationErrorMessage}`, + ); + expect(logError).toHaveBeenNthCalledWith( + 2, + `Error moving issue. Error message: ${mockMutationErrorMessage}`, + ); + + // Only one flash is created even if multiple errors are reported + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was an error while moving the issues.', + }); + }); + + it('creates a flash but not logs errors when a mutation is rejected', async () => { + createComponent({ selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(logError).not.toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith({ + message: 'There was an error while moving the issues.', + }); + }); + }); + + describe('calls mutations', () => { + it('does not call any mutation when no issue is selected', async () => { + createComponent({}, resolvedMutationWithoutErrorsMock); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled(); + }); + + it('does not call any mutation when only tasks are selected', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.tasksOnly }, + resolvedMutationWithoutErrorsMock, + ); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled(); + }); + + it('does not call any mutation when only test cases are selected', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.testCasesOnly }, + resolvedMutationWithoutErrorsMock, + ); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled(); + }); + + it('does not call any mutation when only tasks and test cases are selected', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.tasksAndTestCases }, + resolvedMutationWithoutErrorsMock, + ); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + expect(resolvedMutationWithoutErrorsMock).not.toHaveBeenCalled(); + }); + + it('calls a mutation for every selected issue skipping tasks', async () => { + createComponent( + { selectedIssuables: selectedIssuesMocks.issuesTasksAndTestCases }, + resolvedMutationWithoutErrorsMock, + ); + emitMoveIssuablesEvent(); + + await waitForPromises(); + + // We mock three elements but only two are valid issues since the task is skipped + expect(resolvedMutationWithoutErrorsMock).toHaveBeenCalledTimes(2); + expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(1, { + moveIssueInput: { + projectPath: mockDefaultProps.projectFullPath, + iid: selectedIssuesMocks.issuesTasksAndTestCases[0].iid.toString(), + targetProjectPath: mockDestinationProject.full_path, + }, + }); + + expect(resolvedMutationWithoutErrorsMock).toHaveBeenNthCalledWith(2, { + moveIssueInput: { + projectPath: mockDefaultProps.projectFullPath, + iid: selectedIssuesMocks.issuesTasksAndTestCases[1].iid.toString(), + targetProjectPath: mockDestinationProject.full_path, + }, + }); + }); + }); + }); +}); diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js index 3b79739630d..27707f8b01a 100644 --- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -253,7 +253,7 @@ describe('Pipeline editor tabs component', () => { appStatus | editor | viz | validate | merged ${undefined} | ${true} | ${true} | ${true} | ${true} ${EDITOR_APP_STATUS_EMPTY} | ${true} | ${false} | ${true} | ${false} - ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${false} + ${EDITOR_APP_STATUS_INVALID} | ${true} | ${false} | ${true} | ${true} ${EDITOR_APP_STATUS_VALID} | ${true} | ${true} | ${true} | ${true} `( 'when status is $appStatus, we show - editor:$editor | viz:$viz | validate:$validate | merged:$merged', diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js index bf4026b65db..213ace4a7d6 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js @@ -12,7 +12,7 @@ import { import Protection from '~/projects/settings/branch_rules/components/view/protection.vue'; import branchRulesQuery from '~/projects/settings/branch_rules/queries/branch_rules_details.query.graphql'; import { sprintf } from '~/locale'; -import { branchProtectionsMockResponse } from './mock_data'; +import { branchProtectionsMockResponse, approvalRulesMock } from './mock_data'; jest.mock('~/lib/utils/url_utility', () => ({ getParameterByName: jest.fn().mockReturnValue('main'), @@ -105,9 +105,10 @@ describe('View branch rules', () => { expect(findApprovalsTitle().exists()).toBe(true); expect(findBranchProtections().at(2).props()).toMatchObject({ - header: sprintf(I18N.approvalsHeader, { total: 0 }), + header: sprintf(I18N.approvalsHeader, { total: 3 }), headerLinkHref: approvalRulesPath, headerLinkTitle: I18N.manageApprovalsLinkTitle, + approvals: approvalRulesMock, }); }); }); diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js index c3f573061da..81693b5fa46 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js @@ -1,29 +1,34 @@ const usersMock = [ { + id: '123', username: 'usr1', webUrl: 'http://test.test/usr1', name: 'User 1', avatarUrl: 'http://test.test/avt1.png', }, { + id: '456', username: 'usr2', webUrl: 'http://test.test/usr2', name: 'User 2', avatarUrl: 'http://test.test/avt2.png', }, { + id: '789', username: 'usr3', webUrl: 'http://test.test/usr3', name: 'User 3', avatarUrl: 'http://test.test/avt3.png', }, { + id: '987', username: 'usr4', webUrl: 'http://test.test/usr4', name: 'User 4', avatarUrl: 'http://test.test/avt4.png', }, { + id: '654', username: 'usr5', webUrl: 'http://test.test/usr5', name: 'User 5', @@ -40,6 +45,17 @@ const approvalsRequired = 3; const groupsMock = [{ name: 'test_group_1' }, { name: 'test_group_2' }]; +export const approvalRulesMock = [ + { + __typename: 'ApprovalProjectRule', + id: '123', + name: 'test', + type: 'REGULAR', + eligibleApprovers: { nodes: usersMock }, + approvalsRequired, + }, +]; + export const protectionPropsMock = { header: 'Test protection', headerLinkTitle: 'Test link title', @@ -47,13 +63,7 @@ export const protectionPropsMock = { roles: accessLevelsMock, users: usersMock, groups: groupsMock, - approvals: [ - { - name: 'test', - eligibleApprovers: { nodes: usersMock }, - approvalsRequired, - }, - ], + approvals: approvalRulesMock, }; export const protectionRowPropsMock = { @@ -116,6 +126,10 @@ export const branchProtectionsMockResponse = { edges: accessLevelsMockResponse, }, }, + approvalRules: { + __typename: 'ApprovalProjectRuleConnection', + nodes: approvalRulesMock, + }, }, { __typename: 'BranchRule', @@ -133,6 +147,10 @@ export const branchProtectionsMockResponse = { edges: [], }, }, + approvalRules: { + __typename: 'ApprovalProjectRuleConnection', + nodes: [], + }, }, ], }, diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js index 6e639f895a8..e091f3e25c3 100644 --- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js +++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js @@ -1,18 +1,9 @@ -import Vue, { nextTick } from 'vue'; -import { GlAlert } from '@gitlab/ui'; -import VueApollo from 'vue-apollo'; -import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json'; import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json'; -import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue'; -import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue'; +import TransferLocations from '~/groups_projects/components/transfer_locations.vue'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; -import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql'; import { getTransferLocations } from '~/api/projects_api'; -import waitForPromises from 'helpers/wait_for_promises'; jest.mock('~/api/projects_api', () => ({ getTransferLocations: jest.fn(), @@ -21,68 +12,36 @@ jest.mock('~/api/projects_api', () => ({ describe('Transfer project form', () => { let wrapper; - const projectId = '1'; + const resourceId = '1'; const confirmButtonText = 'Confirm'; const confirmationPhrase = 'You must construct additional pylons!'; - Vue.use(VueApollo); - - const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse); - const mockResolvedGetTransferLocations = ({ - data = transferLocationsResponsePage1, - page = '1', - nextPage = '2', - prevPage = null, - } = {}) => { - getTransferLocations.mockResolvedValueOnce({ - data, - headers: { - 'x-per-page': '2', - 'x-page': page, - 'x-total': '4', - 'x-total-pages': '2', - 'x-next-page': nextPage, - 'x-prev-page': prevPage, - }, - }); - }; - const mockRejectedGetTransferLocations = () => { - const error = new Error(); - - getTransferLocations.mockRejectedValueOnce(error); - }; - - const createComponent = ({ - requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]], - } = {}) => { + const createComponent = () => { wrapper = shallowMountExtended(TransferProjectForm, { provide: { - projectId, + resourceId, }, propsData: { confirmButtonText, confirmationPhrase, }, - apolloProvider: createMockApollo(requestHandlers), }); }; - const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect); - const showNamespaceSelect = async () => { - findNamespaceSelect().vm.$emit('show'); - await waitForPromises(); - }; + const findTransferLocations = () => wrapper.findComponent(TransferLocations); const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger); - const findAlert = () => wrapper.findComponent(GlAlert); afterEach(() => { wrapper.destroy(); }); - it('renders the namespace selector', () => { + it('renders the namespace selector and passes `groupTransferLocationsApiMethod` prop', () => { createComponent(); - expect(findNamespaceSelect().exists()).toBe(true); + expect(findTransferLocations().exists()).toBe(true); + + findTransferLocations().props('groupTransferLocationsApiMethod')(); + expect(getTransferLocations).toHaveBeenCalled(); }); it('renders the confirm button', () => { @@ -100,220 +59,29 @@ describe('Transfer project form', () => { describe('with a selected namespace', () => { const [selectedItem] = transferLocationsResponsePage1; - const arrange = async () => { - mockResolvedGetTransferLocations(); + beforeEach(() => { createComponent(); - await showNamespaceSelect(); - findNamespaceSelect().vm.$emit('select', selectedItem); - }; + findTransferLocations().vm.$emit('input', selectedItem); + }); - it('emits the `selectNamespace` event when a namespace is selected', async () => { - await arrange(); + it('sets `value` prop on `TransferLocations` component', () => { + expect(findTransferLocations().props('value')).toEqual(selectedItem); + }); + it('emits the `selectTransferLocation` event when a namespace is selected', async () => { const args = [selectedItem.id]; - expect(wrapper.emitted('selectNamespace')).toEqual([args]); + expect(wrapper.emitted('selectTransferLocation')).toEqual([args]); }); it('enables the confirm button', async () => { - await arrange(); - expect(findConfirmDanger().attributes('disabled')).toBeUndefined(); }); it('clicking the confirm button emits the `confirm` event', async () => { - await arrange(); - findConfirmDanger().vm.$emit('confirm'); expect(wrapper.emitted('confirm')).toBeDefined(); }); }); - - describe('when `NamespaceSelect` is opened', () => { - it('fetches user and group namespaces and passes correct props to `NamespaceSelect` component', async () => { - mockResolvedGetTransferLocations(); - createComponent(); - await showNamespaceSelect(); - - const { namespace } = currentUserNamespaceQueryResponse.data.currentUser; - - expect(findNamespaceSelect().props()).toMatchObject({ - userNamespaces: [ - { - id: getIdFromGraphQLId(namespace.id), - humanName: namespace.fullName, - }, - ], - groupNamespaces: transferLocationsResponsePage1.map(({ id, full_name: humanName }) => ({ - id, - humanName, - })), - hasNextPageOfGroups: true, - isLoading: false, - isSearchLoading: false, - shouldFilterNamespaces: false, - }); - }); - - describe('when namespaces have already been fetched', () => { - beforeEach(async () => { - mockResolvedGetTransferLocations(); - createComponent(); - await showNamespaceSelect(); - }); - - it('does not fetch namespaces', async () => { - getTransferLocations.mockClear(); - defaultQueryHandler.mockClear(); - - await showNamespaceSelect(); - - expect(getTransferLocations).not.toHaveBeenCalled(); - expect(defaultQueryHandler).not.toHaveBeenCalled(); - }); - }); - - describe('when `getTransferLocations` API call fails', () => { - it('displays error alert', async () => { - mockRejectedGetTransferLocations(); - createComponent(); - await showNamespaceSelect(); - - expect(findAlert().exists()).toBe(true); - }); - }); - - describe('when `currentUser` GraphQL query fails', () => { - it('displays error alert', async () => { - mockResolvedGetTransferLocations(); - const error = new Error(); - createComponent({ - requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]], - }); - await showNamespaceSelect(); - - expect(findAlert().exists()).toBe(true); - }); - }); - }); - - describe('when `search` event is fired', () => { - const arrange = async () => { - mockResolvedGetTransferLocations(); - createComponent(); - await showNamespaceSelect(); - mockResolvedGetTransferLocations(); - findNamespaceSelect().vm.$emit('search', 'foo'); - await nextTick(); - }; - - it('sets `isSearchLoading` prop to `true`', async () => { - await arrange(); - - expect(findNamespaceSelect().props('isSearchLoading')).toBe(true); - }); - - it('passes `search` param to API call', async () => { - await arrange(); - - await waitForPromises(); - - expect(getTransferLocations).toHaveBeenCalledWith( - projectId, - expect.objectContaining({ search: 'foo' }), - ); - }); - - describe('when `getTransferLocations` API call fails', () => { - it('displays dismissible error alert', async () => { - mockResolvedGetTransferLocations(); - createComponent(); - await showNamespaceSelect(); - mockRejectedGetTransferLocations(); - findNamespaceSelect().vm.$emit('search', 'foo'); - await waitForPromises(); - - const alert = findAlert(); - - expect(alert.exists()).toBe(true); - - alert.vm.$emit('dismiss'); - await nextTick(); - - expect(alert.exists()).toBe(false); - }); - }); - }); - - describe('when `load-more-groups` event is fired', () => { - const arrange = async () => { - mockResolvedGetTransferLocations(); - createComponent(); - await showNamespaceSelect(); - - mockResolvedGetTransferLocations({ - data: transferLocationsResponsePage2, - page: '2', - nextPage: null, - prevPage: '1', - }); - - findNamespaceSelect().vm.$emit('load-more-groups'); - await nextTick(); - }; - - it('sets `isLoading` prop to `true`', async () => { - await arrange(); - - expect(findNamespaceSelect().props('isLoading')).toBe(true); - }); - - it('passes `page` param to API call', async () => { - await arrange(); - - await waitForPromises(); - - expect(getTransferLocations).toHaveBeenCalledWith( - projectId, - expect.objectContaining({ page: 2 }), - ); - }); - - it('updates `groupNamespaces` prop with new groups', async () => { - await arrange(); - - await waitForPromises(); - - expect(findNamespaceSelect().props('groupNamespaces')).toMatchObject( - [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map( - ({ id, full_name: humanName }) => ({ - id, - humanName, - }), - ), - ); - }); - - it('updates `hasNextPageOfGroups` prop', async () => { - await arrange(); - - await waitForPromises(); - - expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false); - }); - - describe('when `getTransferLocations` API call fails', () => { - it('displays error alert', async () => { - mockResolvedGetTransferLocations(); - createComponent(); - await showNamespaceSelect(); - mockRejectedGetTransferLocations(); - findNamespaceSelect().vm.$emit('load-more-groups'); - await waitForPromises(); - - expect(findAlert().exists()).toBe(true); - }); - }); - }); }); diff --git a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js index 9b1316677d7..d531147c0e6 100644 --- a/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/sidebar/issuable_move_dropdown_spec.js @@ -37,6 +37,7 @@ const mockProps = { dropdownButtonTitle: 'Move issuable', dropdownHeaderTitle: 'Move issuable', moveInProgress: false, + disabled: false, }; const mockEvent = { @@ -44,20 +45,21 @@ const mockEvent = { preventDefault: jest.fn(), }; -const createComponent = (propsData = mockProps) => - shallowMount(IssuableMoveDropdown, { - propsData, - }); - describe('IssuableMoveDropdown', () => { let mock; let wrapper; - beforeEach(() => { - mock = new MockAdapter(axios); - wrapper = createComponent(); + const createComponent = (propsData = mockProps) => { + wrapper = shallowMount(IssuableMoveDropdown, { + propsData, + }); wrapper.vm.$refs.dropdown.hide = jest.fn(); wrapper.vm.$refs.searchInput.focusInput = jest.fn(); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + createComponent(); }); afterEach(() => { @@ -194,6 +196,12 @@ describe('IssuableMoveDropdown', () => { expect(findDropdownEl().findComponent(GlDropdownForm).exists()).toBe(true); }); + it('renders disabled dropdown when `disabled` is true', () => { + createComponent({ ...mockProps, disabled: true }); + + expect(findDropdownEl().attributes('disabled')).toBe('true'); + }); + it('renders header element', () => { const headerEl = findDropdownEl().find('[data-testid="header"]'); diff --git a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb index 2a6d0825e5c..d5a37f53e21 100644 --- a/spec/lib/api/entities/ml/mlflow/run_info_spec.rb +++ b/spec/lib/api/entities/ml/mlflow/run_info_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe API::Entities::Ml::Mlflow::RunInfo do let_it_be(:candidate) { create(:ml_candidates) } - subject { described_class.new(candidate).as_json } + subject { described_class.new(candidate, packages_url: 'http://example.com').as_json } context 'when start_time is nil' do it { expect(subject[:start_time]).to eq(0) } @@ -53,7 +53,7 @@ RSpec.describe API::Entities::Ml::Mlflow::RunInfo do describe 'artifact_uri' do it 'is not implemented' do - expect(subject[:artifact_uri]).to eq('not_implemented') + expect(subject[:artifact_uri]).to eq("http://example.com#{candidate.artifact_root}") end end diff --git a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb index 9ea519d367e..dc17dc594a8 100644 --- a/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb +++ b/spec/lib/bulk_imports/common/pipelines/entity_finisher_spec.rb @@ -16,8 +16,10 @@ RSpec.describe BulkImports::Common::Pipelines::EntityFinisher do bulk_import_id: entity.bulk_import_id, bulk_import_entity_id: entity.id, bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_class: described_class.name, message: 'Entity finished', + source_version: entity.bulk_import.source_version_info.to_s, importer: 'gitlab_migration' ) end diff --git a/spec/lib/bulk_imports/pipeline/runner_spec.rb b/spec/lib/bulk_imports/pipeline/runner_spec.rb index a5a01354d0e..e66f2d26911 100644 --- a/spec/lib/bulk_imports/pipeline/runner_spec.rb +++ b/spec/lib/bulk_imports/pipeline/runner_spec.rb @@ -55,14 +55,21 @@ RSpec.describe BulkImports::Pipeline::Runner do expect_next_instance_of(Gitlab::Import::Logger) do |logger| expect(logger).to receive(:error) .with( - log_params( - context, - pipeline_step: :extractor, - pipeline_class: 'BulkImports::MyPipeline', - exception_class: exception_class, - exception_message: exception_message, - message: "Pipeline failed", - importer: 'gitlab_migration' + a_hash_including( + 'bulk_import_entity_id' => entity.id, + 'bulk_import_id' => entity.bulk_import_id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, + 'pipeline_step' => :extractor, + 'pipeline_class' => 'BulkImports::MyPipeline', + 'exception.class' => exception_class, + 'exception.message' => exception_message, + 'correlation_id' => anything, + 'class' => 'BulkImports::MyPipeline', + 'message' => "Pipeline failed", + 'importer' => 'gitlab_migration', + 'exception.backtrace' => anything, + 'source_version' => entity.bulk_import.source_version_info.to_s ) ) end @@ -296,6 +303,8 @@ RSpec.describe BulkImports::Pipeline::Runner do bulk_import_id: context.bulk_import_id, bulk_import_entity_id: context.entity.id, bulk_import_entity_type: context.entity.source_type, + source_full_path: entity.source_full_path, + source_version: context.entity.bulk_import.source_version_info.to_s, importer: 'gitlab_migration', context_extra: context.extra }.merge(extra) diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index 186d4e1fb42..f83ce01c617 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -7,7 +7,6 @@ RSpec.describe Gitlab::BitbucketImport::Importer do before do stub_omniauth_provider('bitbucket') - stub_feature_flags(stricter_mr_branch_name: false) end let(:statuses) do diff --git a/spec/lib/gitlab/kas/client_spec.rb b/spec/lib/gitlab/kas/client_spec.rb index 5b89023cc13..9a0fa6c4067 100644 --- a/spec/lib/gitlab/kas/client_spec.rb +++ b/spec/lib/gitlab/kas/client_spec.rb @@ -111,11 +111,16 @@ RSpec.describe Gitlab::Kas::Client do describe 'with grpcs' do let(:stub) { instance_double(Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub) } + let(:credentials) { instance_double(GRPC::Core::ChannelCredentials) } let(:kas_url) { 'grpcs://example.kas.internal' } - it 'uses a ChannelCredentials object' do + it 'uses a ChannelCredentials object with the correct certificates' do + expect(GRPC::Core::ChannelCredentials).to receive(:new) + .with(Gitlab::X509::Certificate.ca_certs_bundle) + .and_return(credentials) + expect(Gitlab::Agent::ConfigurationProject::Rpc::ConfigurationProject::Stub).to receive(:new) - .with('example.kas.internal', instance_of(GRPC::Core::ChannelCredentials), timeout: described_class::TIMEOUT) + .with('example.kas.internal', credentials, timeout: described_class::TIMEOUT) .and_return(stub) allow(stub).to receive(:list_agent_config_files) diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb index db0814af422..8990aa94b47 100644 --- a/spec/models/diff_viewer/server_side_spec.rb +++ b/spec/models/diff_viewer/server_side_spec.rb @@ -16,34 +16,6 @@ RSpec.describe DiffViewer::ServerSide do subject { viewer_class.new(diff_file) } - describe '#prepare!' do - before do - stub_feature_flags(disable_load_entire_blob_for_diff_viewer: feature_flag_enabled) - end - - context 'when the disable_load_entire_blob_for_diff_viewer flag is disabled' do - let(:feature_flag_enabled) { false } - - it 'loads all diff file data' do - subject - expect(diff_file).to receive_message_chain(:old_blob, :load_all_data!) - expect(diff_file).to receive_message_chain(:new_blob, :load_all_data!) - subject.prepare! - end - end - - context 'when the disable_load_entire_blob_for_diff_viewer flag is enabled' do - let(:feature_flag_enabled) { true } - - it 'does not load file data' do - subject - expect(diff_file).not_to receive(:old_blob) - expect(diff_file).not_to receive(:new_blob) - subject.prepare! - end - end - end - describe '#render_error' do context 'when the diff file is stored externally' do before do diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb index de6ce3ba053..0f9d5bf3c2d 100644 --- a/spec/models/experiment_spec.rb +++ b/spec/models/experiment_spec.rb @@ -3,12 +3,9 @@ require 'spec_helper' RSpec.describe Experiment do - include AfterNextHelpers - subject { build(:experiment) } describe 'associations' do - it { is_expected.to have_many(:experiment_users) } it { is_expected.to have_many(:experiment_subjects) } end @@ -18,223 +15,6 @@ RSpec.describe Experiment do it { is_expected.to validate_length_of(:name).is_at_most(255) } end - describe '.add_user' do - let_it_be(:experiment_name) { :experiment_key } - let_it_be(:user) { 'a user' } - let_it_be(:group) { 'a group' } - let_it_be(:context) { { a: 42 } } - - subject(:add_user) { described_class.add_user(experiment_name, group, user, context) } - - context 'when an experiment with the provided name does not exist' do - it 'creates a new experiment record' do - allow_next_instance_of(described_class) do |experiment| - allow(experiment).to receive(:record_user_and_group).with(user, group, context) - end - expect { add_user }.to change(described_class, :count).by(1) - end - - it 'forwards the user, group_type, and context to the instance' do - expect_next_instance_of(described_class) do |experiment| - expect(experiment).to receive(:record_user_and_group).with(user, group, context) - end - add_user - end - end - - context 'when an experiment with the provided name already exists' do - let_it_be(:experiment) { create(:experiment, name: experiment_name) } - - it 'does not create a new experiment record' do - allow_next_found_instance_of(described_class) do |experiment| - allow(experiment).to receive(:record_user_and_group).with(user, group, context) - end - expect { add_user }.not_to change(described_class, :count) - end - - it 'forwards the user, group_type, and context to the instance' do - expect_next_found_instance_of(described_class) do |experiment| - expect(experiment).to receive(:record_user_and_group).with(user, group, context) - end - add_user - end - end - - it 'works without the optional context argument' do - allow_next_instance_of(described_class) do |experiment| - expect(experiment).to receive(:record_user_and_group).with(user, group, {}) - end - - expect { described_class.add_user(experiment_name, group, user) }.not_to raise_error - end - end - - describe '.add_group' do - let_it_be(:experiment_name) { :experiment_key } - let_it_be(:variant) { :control } - let_it_be(:group) { build(:group) } - - subject(:add_group) { described_class.add_group(experiment_name, variant: variant, group: group) } - - context 'when an experiment with the provided name does not exist' do - it 'creates a new experiment record' do - allow_next(described_class, name: :experiment_key) - .to receive(:record_subject_and_variant!).with(group, variant) - - expect { add_group }.to change(described_class, :count).by(1) - end - end - - context 'when an experiment with the provided name already exists' do - before do - create(:experiment, name: experiment_name) - end - - it 'does not create a new experiment record' do - expect { add_group }.not_to change(described_class, :count) - end - end - end - - describe '.record_conversion_event' do - let_it_be(:user) { build(:user) } - let_it_be(:context) { { a: 42 } } - - let(:experiment_key) { :test_experiment } - - subject(:record_conversion_event) { described_class.record_conversion_event(experiment_key, user, context) } - - context 'when no matching experiment exists' do - it 'creates the experiment and uses it' do - expect_next_instance_of(described_class) do |experiment| - expect(experiment).to receive(:record_conversion_event_for_user) - end - expect { record_conversion_event }.to change { described_class.count }.by(1) - end - - context 'but we are unable to successfully create one' do - let(:experiment_key) { nil } - - it 'raises a RecordInvalid error' do - expect { record_conversion_event }.to raise_error(ActiveRecord::RecordInvalid) - end - end - end - - context 'when a matching experiment already exists' do - before do - create(:experiment, name: experiment_key) - end - - it 'sends record_conversion_event_for_user to the experiment instance' do - expect_next_found_instance_of(described_class) do |experiment| - expect(experiment).to receive(:record_conversion_event_for_user).with(user, context) - end - record_conversion_event - end - end - end - - shared_examples 'experiment user with context' do - let_it_be(:context) { { a: 42, 'b' => 34, 'c': { c1: 100, c2: 'c2', e: :e }, d: [1, 3] } } - let_it_be(:initial_expected_context) { { 'a' => 42, 'b' => 34, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [1, 3] } } - - before do - subject - experiment.record_user_and_group(user, :experimental, {}) - end - - it 'has an initial context with stringified keys' do - expect(ExperimentUser.last.context).to eq(initial_expected_context) - end - - context 'when updated' do - before do - subject - experiment.record_user_and_group(user, :experimental, new_context) - end - - context 'with an empty context' do - let_it_be(:new_context) { {} } - - it 'keeps the initial context' do - expect(ExperimentUser.last.context).to eq(initial_expected_context) - end - end - - context 'with string keys' do - let_it_be(:new_context) { { f: :some_symbol } } - - it 'adds new symbols stringified' do - expected_context = initial_expected_context.merge('f' => 'some_symbol') - expect(ExperimentUser.last.context).to eq(expected_context) - end - end - - context 'with atomic values or array values' do - let_it_be(:new_context) { { b: 97, d: [99] } } - - it 'overrides the values' do - expected_context = { 'a' => 42, 'b' => 97, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [99] } - expect(ExperimentUser.last.context).to eq(expected_context) - end - end - - context 'with nested hashes' do - let_it_be(:new_context) { { c: { g: 107 } } } - - it 'inserts nested additional values in the same keys' do - expected_context = initial_expected_context.deep_merge('c' => { 'g' => 107 }) - expect(ExperimentUser.last.context).to eq(expected_context) - end - end - end - end - - describe '#record_conversion_event_for_user' do - let_it_be(:user) { create(:user) } - let_it_be(:experiment) { create(:experiment) } - let_it_be(:context) { { a: 42 } } - - subject { experiment.record_conversion_event_for_user(user, context) } - - context 'when no existing experiment_user record exists for the given user' do - it 'does not update or create an experiment_user record' do - expect { subject }.not_to change { ExperimentUser.all.to_a } - end - end - - context 'when an existing experiment_user exists for the given user' do - context 'but it has already been converted' do - let!(:experiment_user) { create(:experiment_user, experiment: experiment, user: user, converted_at: 2.days.ago) } - - it 'does not update the converted_at value' do - expect { subject }.not_to change { experiment_user.converted_at } - end - - it_behaves_like 'experiment user with context' do - before do - experiment.record_user_and_group(user, :experimental, context) - end - end - end - - context 'and it has not yet been converted' do - let(:experiment_user) { create(:experiment_user, experiment: experiment, user: user) } - - it 'updates the converted_at value' do - expect { subject }.to change { experiment_user.reload.converted_at } - end - - it_behaves_like 'experiment user with context' do - before do - experiment.record_user_and_group(user, :experimental, context) - end - end - end - end - end - describe '#record_conversion_event_for_subject' do let_it_be(:user) { create(:user) } let_it_be(:experiment) { create(:experiment) } @@ -367,62 +147,4 @@ RSpec.describe Experiment do end end end - - describe '#record_user_and_group' do - let_it_be(:experiment) { create(:experiment) } - let_it_be(:user) { create(:user) } - let_it_be(:group) { :control } - let_it_be(:context) { { a: 42 } } - - subject { experiment.record_user_and_group(user, group, context) } - - context 'when an experiment_user does not yet exist for the given user' do - it 'creates a new experiment_user record' do - expect { subject }.to change(ExperimentUser, :count).by(1) - end - - it 'assigns the correct group_type to the experiment_user' do - subject - - expect(ExperimentUser.last.group_type).to eq('control') - end - - it 'adds the correct context to the experiment_user' do - subject - - expect(ExperimentUser.last.context).to eq({ 'a' => 42 }) - end - end - - context 'when an experiment_user already exists for the given user' do - before do - # Create an existing experiment_user for this experiment and the :control group - experiment.record_user_and_group(user, :control) - end - - it 'does not create a new experiment_user record' do - expect { subject }.not_to change(ExperimentUser, :count) - end - - context 'when group type or context did not change' do - let(:context) { {} } - - it 'does not initiate a transaction' do - expect(Experiment.connection).not_to receive(:transaction) - - subject - end - end - - context 'but the group_type and context has changed' do - let(:group) { :experimental } - - it 'updates the existing experiment_user record with group_type' do - expect { subject }.to change { ExperimentUser.last.group_type } - end - end - - it_behaves_like 'experiment user with context' - end - end end diff --git a/spec/models/experiment_user_spec.rb b/spec/models/experiment_user_spec.rb deleted file mode 100644 index 9201529b145..00000000000 --- a/spec/models/experiment_user_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe ExperimentUser do - describe 'Associations' do - it { is_expected.to belong_to(:experiment) } - it { is_expected.to belong_to(:user) } - end - - describe 'Validations' do - it { is_expected.to validate_presence_of(:group_type) } - end -end diff --git a/spec/models/integrations/datadog_spec.rb b/spec/models/integrations/datadog_spec.rb index 71a5bbc4db1..65ecd9bee83 100644 --- a/spec/models/integrations/datadog_spec.rb +++ b/spec/models/integrations/datadog_spec.rb @@ -73,7 +73,15 @@ RSpec.describe Integrations::Datadog do it { is_expected.to validate_presence_of(:datadog_site) } it { is_expected.not_to validate_presence_of(:api_url) } + it { is_expected.to allow_value('data-dog-hq.com').for(:datadog_site) } + it { is_expected.to allow_value('dataDOG.com').for(:datadog_site) } it { is_expected.not_to allow_value('datadog hq.com').for(:datadog_site) } + it { is_expected.not_to allow_value('-datadoghq.com').for(:datadog_site) } + it { is_expected.not_to allow_value('.datadoghq.com').for(:datadog_site) } + it { is_expected.not_to allow_value('datadoghq.com_').for(:datadog_site) } + it { is_expected.not_to allow_value('data-dog').for(:datadog_site) } + it { is_expected.not_to allow_value('datadoghq.com-').for(:datadog_site) } + it { is_expected.not_to allow_value('datadoghq.com.').for(:datadog_site) } end context 'with custom api_url' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 21bba0c270d..665e62b82b9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -232,10 +232,6 @@ RSpec.describe MergeRequest, factory_default: :keep do end context 'for branch' do - before do - stub_feature_flags(stricter_mr_branch_name: false) - end - where(:branch_name, :valid) do 'foo' | true 'foo:bar' | false diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb index 3bf1e80a152..84e0e6be199 100644 --- a/spec/models/ml/candidate_spec.rb +++ b/spec/models/ml/candidate_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Ml::Candidate, factory_default: :keep do - let_it_be(:candidate) { create_default(:ml_candidates, :with_metrics_and_params) } + let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params) } describe 'associations' do it { is_expected.to belong_to(:experiment) } @@ -12,6 +12,12 @@ RSpec.describe Ml::Candidate, factory_default: :keep do it { is_expected.to have_many(:metrics) } end + describe '.artifact_root' do + subject { candidate.artifact_root } + + it { is_expected.to eq("/ml_candidate_#{candidate.iid}/-/") } + end + describe '#new' do it 'iid is not null' do expect(candidate.iid).not_to be_nil diff --git a/spec/requests/api/ml/mlflow_spec.rb b/spec/requests/api/ml/mlflow_spec.rb index 09e9359c0b3..c588c99c1bf 100644 --- a/spec/requests/api/ml/mlflow_spec.rb +++ b/spec/requests/api/ml/mlflow_spec.rb @@ -295,7 +295,6 @@ RSpec.describe API::Ml::Mlflow do 'experiment_id' => params[:experiment_id], 'user_id' => current_user.id.to_s, 'start_time' => params[:start_time], - 'artifact_uri' => 'not_implemented', 'status' => "RUNNING", 'lifecycle_stage' => "active" } @@ -339,7 +338,7 @@ RSpec.describe API::Ml::Mlflow do 'experiment_id' => candidate.experiment.iid.to_s, 'user_id' => candidate.user.id.to_s, 'start_time' => candidate.start_time, - 'artifact_uri' => 'not_implemented', + 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.iid}/-/", 'status' => "RUNNING", 'lifecycle_stage' => "active" } @@ -378,7 +377,7 @@ RSpec.describe API::Ml::Mlflow do 'user_id' => candidate.user.id.to_s, 'start_time' => candidate.start_time, 'end_time' => params[:end_time], - 'artifact_uri' => 'not_implemented', + 'artifact_uri' => "http://www.example.com/api/v4/projects/#{project_id}/packages/generic/ml_candidate_#{candidate.iid}/-/", 'status' => 'FAILED', 'lifecycle_stage' => 'active' } diff --git a/spec/requests/projects/merge_requests/diffs_spec.rb b/spec/requests/projects/merge_requests/diffs_spec.rb index 9f0b9a9cb1b..57548a3c83f 100644 --- a/spec/requests/projects/merge_requests/diffs_spec.rb +++ b/spec/requests/projects/merge_requests/diffs_spec.rb @@ -80,6 +80,20 @@ RSpec.describe 'Merge Requests Diffs' do expect(response).to have_gitlab_http_status(:not_modified) end + context 'with check_etags_diffs_batch_before_write_cache flag turned off' do + before do + stub_feature_flags(check_etags_diffs_batch_before_write_cache: false) + end + + it 'does not serialize diffs' do + expect(PaginatedDiffSerializer).not_to receive(:new) + + go(headers: headers, page: 0, per_page: 5) + + expect(response).to have_gitlab_http_status(:not_modified) + end + end + context 'with the different user' do let(:another_user) { create(:user) } let(:collection) { Gitlab::Diff::FileCollection::MergeRequestDiffBatch } diff --git a/spec/rubocop/cop/gitlab/json_spec.rb b/spec/rubocop/cop/gitlab/json_spec.rb index e4ec107747d..bf8a250b55c 100644 --- a/spec/rubocop/cop/gitlab/json_spec.rb +++ b/spec/rubocop/cop/gitlab/json_spec.rb @@ -5,12 +5,20 @@ require_relative '../../../../rubocop/cop/gitlab/json' RSpec.describe RuboCop::Cop::Gitlab::Json do context 'when ::JSON is called' do - it 'registers an offense' do + it 'registers an offense and autocorrects' do expect_offense(<<~RUBY) class Foo def bar JSON.parse('{ "foo": "bar" }') - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid calling `JSON` directly. [...] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` or `to_json` directly. [...] + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo + def bar + Gitlab::Json.parse('{ "foo": "bar" }') end end RUBY @@ -18,12 +26,41 @@ RSpec.describe RuboCop::Cop::Gitlab::Json do end context 'when ActiveSupport::JSON is called' do - it 'registers an offense' do + it 'registers an offense and autocorrects' do expect_offense(<<~RUBY) class Foo def bar ActiveSupport::JSON.parse('{ "foo": "bar" }') - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid calling `JSON` directly. [...] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` or `to_json` directly. [...] + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo + def bar + Gitlab::Json.parse('{ "foo": "bar" }') + end + end + RUBY + end + end + + context 'when .to_json is called' do + it 'registers an offense and autocorrects' do + expect_offense(<<~RUBY) + class Foo + def bar + { foo: "bar" }.to_json + ^^^^^^^^^^^^^^^^^^^^^^ Prefer `Gitlab::Json` over calling `JSON` or `to_json` directly. [...] + end + end + RUBY + + expect_correction(<<~RUBY) + class Foo + def bar + Gitlab::Json.generate({ foo: "bar" }) end end RUBY diff --git a/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb index 0de962328c5..5a7852fc32f 100644 --- a/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb +++ b/spec/services/bulk_imports/create_pipeline_trackers_service_spec.rb @@ -77,6 +77,8 @@ RSpec.describe BulkImports::CreatePipelineTrackersService do message: 'Pipeline skipped as source instance version not compatible with pipeline', bulk_import_entity_id: entity.id, bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, importer: 'gitlab_migration', pipeline_name: 'PipelineClass4', minimum_source_version: '15.1.0', @@ -88,6 +90,8 @@ RSpec.describe BulkImports::CreatePipelineTrackersService do message: 'Pipeline skipped as source instance version not compatible with pipeline', bulk_import_entity_id: entity.id, bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, importer: 'gitlab_migration', pipeline_name: 'PipelineClass5', minimum_source_version: '16.0.0', diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb index 7bf6dfd0fd8..43d23023d83 100644 --- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb +++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb @@ -244,9 +244,10 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end shared_examples 'process truncated alerts' do - it 'returns 200 but skips processing and logs a warning', :aggregate_failures do + it 'returns 201 but skips processing and logs a warning', :aggregate_failures do expect(subject).to be_success - expect(subject.payload[:alerts].size).to eq(max_alerts) + expect(subject.payload).to eq({}) + expect(subject.http_status).to eq(:created) expect(Gitlab::AppLogger) .to have_received(:warn) .with( @@ -260,9 +261,10 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end shared_examples 'process all alerts' do - it 'returns 200 and process alerts without warnings', :aggregate_failures do + it 'returns 201 and process alerts without warnings', :aggregate_failures do expect(subject).to be_success - expect(subject.payload[:alerts].size).to eq(2) + expect(subject.payload).to eq({}) + expect(subject.http_status).to eq(:created) expect(Gitlab::AppLogger).not_to have_received(:warn) end end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index c4377e368ee..0558bfd51c3 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -10917,7 +10917,6 @@ - './spec/workers/environments/canary_ingress/update_worker_spec.rb' - './spec/workers/error_tracking_issue_link_worker_spec.rb' - './spec/workers/every_sidekiq_worker_spec.rb' -- './spec/workers/experiments/record_conversion_event_worker_spec.rb' - './spec/workers/expire_build_artifacts_worker_spec.rb' - './spec/workers/export_csv_worker_spec.rb' - './spec/workers/external_service_reactive_caching_worker_spec.rb' diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb index 571cb7dc03d..b46ace1824a 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -72,8 +72,8 @@ RSpec.shared_examples 'processes one firing and one resolved prometheus alerts' .and change(Note, :count).by(1) expect(subject).to be_success - expect(subject.payload[:alerts]).to all(be_a_kind_of(AlertManagement::Alert)) - expect(subject.payload[:alerts].size).to eq(1) + expect(subject.payload).to eq({}) + expect(subject.http_status).to eq(:created) end it_behaves_like 'processes incident issues' diff --git a/spec/workers/bulk_imports/entity_worker_spec.rb b/spec/workers/bulk_imports/entity_worker_spec.rb index 0fcdbccc304..e3f0ee65205 100644 --- a/spec/workers/bulk_imports/entity_worker_spec.rb +++ b/spec/workers/bulk_imports/entity_worker_spec.rb @@ -39,8 +39,11 @@ RSpec.describe BulkImports::EntityWorker do hash_including( 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, 'current_stage' => nil, 'message' => 'Stage starting', + 'source_version' => entity.bulk_import.source_version_info.to_s, 'importer' => 'gitlab_migration' ) ) @@ -71,7 +74,10 @@ RSpec.describe BulkImports::EntityWorker do hash_including( 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, 'current_stage' => nil, + 'source_version' => entity.bulk_import.source_version_info.to_s, 'importer' => 'gitlab_migration' ) ) @@ -82,9 +88,15 @@ RSpec.describe BulkImports::EntityWorker do hash_including( 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, 'current_stage' => nil, - 'message' => 'Error!', - 'importer' => 'gitlab_migration' + 'message' => 'Entity failed', + 'exception.backtrace' => anything, + 'exception.class' => 'StandardError', + 'exception.message' => 'Error!', + 'importer' => 'gitlab_migration', + 'source_version' => entity.bulk_import.source_version_info.to_s ) ) end @@ -95,6 +107,9 @@ RSpec.describe BulkImports::EntityWorker do exception, bulk_import_entity_id: entity.id, bulk_import_id: entity.bulk_import_id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, + source_version: entity.bulk_import.source_version_info.to_s, importer: 'gitlab_migration' ) @@ -112,8 +127,11 @@ RSpec.describe BulkImports::EntityWorker do hash_including( 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, 'current_stage' => 0, 'message' => 'Stage running', + 'source_version' => entity.bulk_import.source_version_info.to_s, 'importer' => 'gitlab_migration' ) ) @@ -142,7 +160,10 @@ RSpec.describe BulkImports::EntityWorker do hash_including( 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, 'current_stage' => 0, + 'source_version' => entity.bulk_import.source_version_info.to_s, 'importer' => 'gitlab_migration' ) ) diff --git a/spec/workers/bulk_imports/export_request_worker_spec.rb b/spec/workers/bulk_imports/export_request_worker_spec.rb index 597eed3a9b9..7eb8150fb2e 100644 --- a/spec/workers/bulk_imports/export_request_worker_spec.rb +++ b/spec/workers/bulk_imports/export_request_worker_spec.rb @@ -20,7 +20,7 @@ RSpec.describe BulkImports::ExportRequestWorker do end shared_examples 'requests relations export for api resource' do - include_examples 'an idempotent worker' do + it_behaves_like 'an idempotent worker' do it 'requests relations export & schedules entity worker' do expect_next_instance_of(BulkImports::Clients::HTTP) do |client| expect(client).to receive(:post).with(expected).twice @@ -44,18 +44,22 @@ RSpec.describe BulkImports::ExportRequestWorker do it 'logs retry request and reenqueues' do allow(exception).to receive(:retriable?).twice.and_return(true) - expect(Gitlab::Import::Logger).to receive(:error).with( - hash_including( - 'bulk_import_entity_id' => entity.id, - 'pipeline_class' => 'ExportRequestWorker', - 'exception_class' => 'BulkImports::NetworkError', - 'exception_message' => 'Export error', - 'bulk_import_id' => bulk_import.id, - 'bulk_import_entity_type' => entity.source_type, - 'importer' => 'gitlab_migration', - 'message' => 'Retrying export request' - ) - ).twice + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:error).with( + a_hash_including( + 'bulk_import_entity_id' => entity.id, + 'bulk_import_id' => entity.bulk_import_id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, + 'exception.backtrace' => anything, + 'exception.class' => 'BulkImports::NetworkError', + 'exception.message' => 'Export error', + 'message' => 'Retrying export request', + 'importer' => 'gitlab_migration', + 'source_version' => entity.bulk_import.source_version_info.to_s + ) + ).twice + end expect(described_class).to receive(:perform_in).twice.with(2.seconds, entity.id) @@ -65,18 +69,24 @@ RSpec.describe BulkImports::ExportRequestWorker do context 'when error is not retriable' do it 'logs export failure and marks entity as failed' do - expect(Gitlab::Import::Logger).to receive(:error).with( - hash_including( - 'bulk_import_entity_id' => entity.id, - 'pipeline_class' => 'ExportRequestWorker', - 'exception_class' => 'BulkImports::NetworkError', - 'exception_message' => 'Export error', - 'correlation_id_value' => anything, - 'bulk_import_id' => bulk_import.id, - 'bulk_import_entity_type' => entity.source_type, - 'importer' => 'gitlab_migration' - ) - ).twice + allow(exception).to receive(:retriable?).twice.and_return(false) + + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:error).with( + a_hash_including( + 'bulk_import_entity_id' => entity.id, + 'bulk_import_id' => entity.bulk_import_id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, + 'exception.backtrace' => anything, + 'exception.class' => 'BulkImports::NetworkError', + 'exception.message' => 'Export error', + 'message' => "Request to export #{entity.source_type} failed", + 'importer' => 'gitlab_migration', + 'source_version' => entity.bulk_import.source_version_info.to_s + ) + ).twice + end perform_multiple(job_args) @@ -119,25 +129,30 @@ RSpec.describe BulkImports::ExportRequestWorker do let(:entity_source_id) { 'invalid' } it 'logs the error & requests relations export using full path url' do + allow(BulkImports::EntityWorker).to receive(:perform_async) + expect_next_instance_of(BulkImports::Clients::HTTP) do |client| expect(client).to receive(:post).with(full_path_url).twice end entity.update!(source_xid: nil) - expect(Gitlab::Import::Logger).to receive(:error).with( - a_hash_including( - 'message' => 'Failed to fetch source entity id', - 'bulk_import_entity_id' => entity.id, - 'pipeline_class' => 'ExportRequestWorker', - 'exception_class' => 'NoMethodError', - 'exception_message' => "undefined method `model_id' for nil:NilClass", - 'correlation_id_value' => anything, - 'bulk_import_id' => bulk_import.id, - 'bulk_import_entity_type' => entity.source_type, - 'importer' => 'gitlab_migration' - ) - ).twice + expect_next_instance_of(Gitlab::Import::Logger) do |logger| + expect(logger).to receive(:error).with( + a_hash_including( + 'bulk_import_entity_id' => entity.id, + 'bulk_import_id' => entity.bulk_import_id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, + 'exception.backtrace' => anything, + 'exception.class' => 'NoMethodError', + 'exception.message' => "undefined method `model_id' for nil:NilClass", + 'message' => 'Failed to fetch source entity id', + 'importer' => 'gitlab_migration', + 'source_version' => entity.bulk_import.source_version_info.to_s + ) + ).twice + end perform_multiple(job_args) @@ -153,7 +168,7 @@ RSpec.describe BulkImports::ExportRequestWorker do let(:expected) { "/groups/#{entity.source_xid}/export_relations" } let(:full_path_url) { '/groups/foo%2Fbar/export_relations' } - include_examples 'requests relations export for api resource' + it_behaves_like 'requests relations export for api resource' end context 'when entity is project' do @@ -161,7 +176,7 @@ RSpec.describe BulkImports::ExportRequestWorker do let(:expected) { "/projects/#{entity.source_xid}/export_relations" } let(:full_path_url) { '/projects/foo%2Fbar/export_relations' } - include_examples 'requests relations export for api resource' + it_behaves_like 'requests relations export for api resource' end end end diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb index ee65775f170..23fbc5688ec 100644 --- a/spec/workers/bulk_imports/pipeline_worker_spec.rb +++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb @@ -37,9 +37,10 @@ RSpec.describe BulkImports::PipelineWorker do .with( hash_including( 'pipeline_name' => 'FakePipeline', - 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, - 'importer' => 'gitlab_migration' + 'bulk_import_entity_id' => entity.id, + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path ) ) end @@ -87,8 +88,10 @@ RSpec.describe BulkImports::PipelineWorker do 'pipeline_tracker_id' => pipeline_tracker.id, 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, - 'message' => 'Unstarted pipeline not found', - 'importer' => 'gitlab_migration' + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, + 'source_version' => entity.bulk_import.source_version_info.to_s, + 'message' => 'Unstarted pipeline not found' ) ) end @@ -126,7 +129,13 @@ RSpec.describe BulkImports::PipelineWorker do 'pipeline_name' => 'FakePipeline', 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, - 'message' => 'Error!', + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, + 'class' => 'BulkImports::PipelineWorker', + 'exception.backtrace' => anything, + 'exception.message' => 'Error!', + 'message' => 'Pipeline failed', + 'source_version' => entity.bulk_import.source_version_info.to_s, 'importer' => 'gitlab_migration' ) ) @@ -137,9 +146,12 @@ RSpec.describe BulkImports::PipelineWorker do .with( instance_of(StandardError), bulk_import_entity_id: entity.id, - bulk_import_id: entity.bulk_import_id, + bulk_import_id: entity.bulk_import.id, + bulk_import_entity_type: entity.source_type, + source_full_path: entity.source_full_path, pipeline_name: pipeline_tracker.pipeline_name, - importer: 'gitlab_migration' + importer: 'gitlab_migration', + source_version: entity.bulk_import.source_version_info.to_s ) expect(BulkImports::EntityWorker) @@ -188,8 +200,9 @@ RSpec.describe BulkImports::PipelineWorker do 'pipeline_name' => 'FakePipeline', 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, - 'message' => 'Skipping pipeline due to failed entity', - 'importer' => 'gitlab_migration' + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, + 'message' => 'Skipping pipeline due to failed entity' ) ) end @@ -237,7 +250,8 @@ RSpec.describe BulkImports::PipelineWorker do 'pipeline_name' => 'FakePipeline', 'bulk_import_entity_id' => entity.id, 'bulk_import_id' => entity.bulk_import_id, - 'importer' => 'gitlab_migration' + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path ) ) end @@ -361,9 +375,16 @@ RSpec.describe BulkImports::PipelineWorker do hash_including( 'pipeline_name' => 'NdjsonPipeline', 'bulk_import_entity_id' => entity.id, - 'message' => 'Pipeline timeout', 'bulk_import_id' => entity.bulk_import_id, - 'importer' => 'gitlab_migration' + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, + 'class' => 'BulkImports::PipelineWorker', + 'exception.backtrace' => anything, + 'exception.class' => 'BulkImports::Pipeline::ExpiredError', + 'exception.message' => 'Pipeline timeout', + 'importer' => 'gitlab_migration', + 'message' => 'Pipeline failed', + 'source_version' => entity.bulk_import.source_version_info.to_s ) ) end @@ -390,9 +411,14 @@ RSpec.describe BulkImports::PipelineWorker do hash_including( 'pipeline_name' => 'NdjsonPipeline', 'bulk_import_entity_id' => entity.id, - 'message' => 'Export from source instance failed: Error!', 'bulk_import_id' => entity.bulk_import_id, - 'importer' => 'gitlab_migration' + 'bulk_import_entity_type' => entity.source_type, + 'source_full_path' => entity.source_full_path, + 'exception.backtrace' => anything, + 'exception.class' => 'BulkImports::Pipeline::FailedError', + 'exception.message' => 'Export from source instance failed: Error!', + 'importer' => 'gitlab_migration', + 'source_version' => entity.bulk_import.source_version_info.to_s ) ) end |