diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-09 15:17:20 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-09 15:17:20 +0000 |
commit | 3670ddd229b178c0a2e09a1466ddfd7fd2f7855d (patch) | |
tree | 9be2a8155e0b14fb9a07b6a1c8bcfa629af4a25c | |
parent | 0b4adad74b76b34855e9a6d943f9b9188c3914fa (diff) | |
download | gitlab-ce-3670ddd229b178c0a2e09a1466ddfd7fd2f7855d.tar.gz |
Add latest changes from gitlab-org/gitlab@master
120 files changed, 1692 insertions, 442 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 31ddb12c65b..72860a91241 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -411,7 +411,7 @@ lib/gitlab/checks/** /doc/administration/environment_variables.md @axil /doc/administration/external_pipeline_validation.md @drcatherinepope /doc/administration/feature_flags.md @axil -/doc/administration/file_hooks.md @ashrafkhamis +/doc/administration/file_hooks.md @eread @ashrafkhamis /doc/administration/geo/ @axil /doc/administration/get_started.md @kpaizee /doc/administration/git_protocol.md @aqualls @@ -436,7 +436,7 @@ lib/gitlab/checks/** /doc/administration/logs/index.md @msedlakjakubowski /doc/administration/maintenance_mode/ @axil /doc/administration/merge_request_diffs.md @aqualls -/doc/administration/monitoring/github_imports.md @eread +/doc/administration/monitoring/github_imports.md @eread @ashrafkhamis /doc/administration/monitoring/gitlab_self_monitoring_project/ @msedlakjakubowski /doc/administration/monitoring/index.md @msedlakjakubowski /doc/administration/monitoring/ip_allowlist.md @jglassman1 @@ -480,7 +480,7 @@ lib/gitlab/checks/** /doc/administration/smime_signing_email.md @axil /doc/administration/snippets/ @aqualls /doc/administration/static_objects_external_storage.md @ashrafkhamis -/doc/administration/system_hooks.md @ashrafkhamis +/doc/administration/system_hooks.md @eread @ashrafkhamis /doc/administration/terraform_state.md @phillipwells /doc/administration/timezone.md @axil /doc/administration/troubleshooting/ @axil @@ -490,7 +490,7 @@ lib/gitlab/checks/** /doc/api/access_requests.md @jglassman1 /doc/api/admin_sidekiq_queues.md @axil /doc/api/alert_management_alerts.md @msedlakjakubowski -/doc/api/api_resources.md @ashrafkhamis +/doc/api/api_resources.md @eread @ashrafkhamis /doc/api/appearance.md @jglassman1 /doc/api/applications.md @jglassman1 /doc/api/audit_events.md @eread @@ -498,7 +498,7 @@ lib/gitlab/checks/** /doc/api/award_emoji.md @msedlakjakubowski /doc/api/boards.md @msedlakjakubowski /doc/api/branches.md @aqualls -/doc/api/bulk_imports.md @eread +/doc/api/bulk_imports.md @eread @ashrafkhamis /doc/api/cluster_agents.md @phillipwells /doc/api/commits.md @aqualls /doc/api/container_registry.md @marcel.amirault @@ -522,13 +522,14 @@ lib/gitlab/checks/** /doc/api/features.md @phillipwells /doc/api/freeze_periods.md @phillipwells /doc/api/geo_nodes.md @axil +/doc/api/geo_sites.md @axil /doc/api/graphql/audit_report.md @eread /doc/api/graphql/branch_rules.md @aqualls /doc/api/graphql/custom_emoji.md @msedlakjakubowski -/doc/api/graphql/getting_started.md @ashrafkhamis -/doc/api/graphql/index.md @ashrafkhamis -/doc/api/graphql/reference/ @ashrafkhamis -/doc/api/graphql/removed_items.md @ashrafkhamis +/doc/api/graphql/getting_started.md @eread @ashrafkhamis +/doc/api/graphql/index.md @eread @ashrafkhamis +/doc/api/graphql/reference/ @eread @ashrafkhamis +/doc/api/graphql/removed_items.md @eread @ashrafkhamis /doc/api/graphql/sample_issue_boards.md @msedlakjakubowski /doc/api/graphql/users_example.md @jglassman1 /doc/api/group_access_tokens.md @jglassman1 @@ -537,22 +538,22 @@ lib/gitlab/checks/** /doc/api/group_boards.md @msedlakjakubowski /doc/api/group_clusters.md @phillipwells /doc/api/group_epic_boards.md @msedlakjakubowski -/doc/api/group_import_export.md @eread +/doc/api/group_import_export.md @eread @ashrafkhamis /doc/api/group_iterations.md @msedlakjakubowski /doc/api/group_labels.md @msedlakjakubowski /doc/api/group_level_variables.md @marcel.amirault /doc/api/group_milestones.md @msedlakjakubowski /doc/api/group_protected_branches.md @aqualls /doc/api/group_protected_environments.md @phillipwells -/doc/api/group_relations_export.md @eread +/doc/api/group_relations_export.md @eread @ashrafkhamis /doc/api/group_releases.md @phillipwells /doc/api/group_repository_storage_moves.md @ashrafkhamis /doc/api/groups.md @lciutacu -/doc/api/import.md @eread -/doc/api/index.md @ashrafkhamis +/doc/api/import.md @eread @ashrafkhamis +/doc/api/index.md @eread @ashrafkhamis /doc/api/instance_clusters.md @phillipwells /doc/api/instance_level_ci_variables.md @marcel.amirault -/doc/api/integrations.md @ashrafkhamis +/doc/api/integrations.md @eread @ashrafkhamis /doc/api/issue_links.md @msedlakjakubowski /doc/api/issues.md @msedlakjakubowski /doc/api/issues_statistics.md @msedlakjakubowski @@ -580,7 +581,7 @@ lib/gitlab/checks/** /doc/api/notes.md @msedlakjakubowski /doc/api/notification_settings.md @msedlakjakubowski /doc/api/oauth2.md @jglassman1 -/doc/api/openapi/ @ashrafkhamis +/doc/api/openapi/ @eread @ashrafkhamis /doc/api/packages.md @marcel.amirault /doc/api/packages/ @marcel.amirault /doc/api/personal_access_tokens.md @eread @@ -595,7 +596,7 @@ lib/gitlab/checks/** /doc/api/project_clusters.md @phillipwells /doc/api/project_import_export.md @aqualls /doc/api/project_level_variables.md @marcel.amirault -/doc/api/project_relations_export.md @eread +/doc/api/project_relations_export.md @eread @ashrafkhamis /doc/api/project_repository_storage_moves.md @eread /doc/api/project_snippets.md @aqualls /doc/api/project_statistics.md @aqualls @@ -616,7 +617,7 @@ lib/gitlab/checks/** /doc/api/resource_milestone_events.md @msedlakjakubowski /doc/api/resource_state_events.md @msedlakjakubowski /doc/api/resource_weight_events.md @msedlakjakubowski -/doc/api/rest/ @ashrafkhamis +/doc/api/rest/ @eread @ashrafkhamis /doc/api/runners.md @fneill /doc/api/saml.md @jglassman1 /doc/api/scim.md @jglassman1 @@ -629,7 +630,7 @@ lib/gitlab/checks/** /doc/api/statistics.md @jglassman1 /doc/api/status_checks.md @eread /doc/api/suggestions.md @aqualls -/doc/api/system_hooks.md @ashrafkhamis +/doc/api/system_hooks.md @eread @ashrafkhamis /doc/api/tags.md @aqualls /doc/api/templates/dockerfiles.md @aqualls /doc/api/templates/gitignores.md @aqualls @@ -649,7 +650,7 @@ lib/gitlab/checks/** /doc/architecture/blueprints/database_scaling/ @aqualls /doc/ci/ @drcatherinepope /doc/ci/caching/ @marcel.amirault -/doc/ci/chatops/ @eread +/doc/ci/chatops/ @eread @ashrafkhamis /doc/ci/cloud_deployment/ @phillipwells /doc/ci/cloud_services/ @marcel.amirault /doc/ci/directed_acyclic_graph/ @marcel.amirault @@ -690,10 +691,10 @@ lib/gitlab/checks/** /doc/development/backend/ @sselhorn /doc/development/backend/create_source_code_be/ @aqualls /doc/development/build_test_package.md @axil -/doc/development/bulk_import.md @eread +/doc/development/bulk_import.md @eread @ashrafkhamis /doc/development/cached_queries.md @jglassman1 /doc/development/cascading_settings.md @jglassman1 -/doc/development/chatops_on_gitlabcom.md @eread +/doc/development/chatops_on_gitlabcom.md @eread @ashrafkhamis /doc/development/cicd/ @marcel.amirault /doc/development/cicd/cicd_tables.md @drcatherinepope /doc/development/cicd/index.md @drcatherinepope @@ -709,7 +710,7 @@ lib/gitlab/checks/** /doc/development/distributed_tracing.md @msedlakjakubowski /doc/development/distribution/ @axil /doc/development/documentation/ @sselhorn -/doc/development/export_csv.md @eread +/doc/development/export_csv.md @eread @ashrafkhamis /doc/development/fe_guide/customizable_dashboards.md @lciutacu /doc/development/fe_guide/dark_mode.md @sselhorn /doc/development/fe_guide/graphql.md @sselhorn @@ -726,13 +727,13 @@ lib/gitlab/checks/** /doc/development/gitaly.md @eread /doc/development/gitlab_flavored_markdown/ @ashrafkhamis /doc/development/gitlab_shell/ @aqualls -/doc/development/graphql_guide/ @ashrafkhamis +/doc/development/graphql_guide/ @eread @ashrafkhamis /doc/development/graphql_guide/batchloader.md @aqualls -/doc/development/i18n/ @eread +/doc/development/i18n/ @eread @ashrafkhamis /doc/development/image_scaling.md @lciutacu -/doc/development/import_export.md @eread +/doc/development/import_export.md @eread @ashrafkhamis /doc/development/index.md @sselhorn -/doc/development/integrations/ @ashrafkhamis +/doc/development/integrations/ @eread @ashrafkhamis /doc/development/integrations/secure.md @rdickenson /doc/development/integrations/secure_partner_integration.md @rdickenson /doc/development/internal_api/ @aqualls @@ -778,21 +779,21 @@ lib/gitlab/checks/** /doc/integration/advanced_search/ @ashrafkhamis /doc/integration/akismet.md @phillipwells /doc/integration/arkose.md @phillipwells -/doc/integration/datadog.md @ashrafkhamis -/doc/integration/external-issue-tracker.md @ashrafkhamis +/doc/integration/datadog.md @eread @ashrafkhamis +/doc/integration/external-issue-tracker.md @eread @ashrafkhamis /doc/integration/gitpod.md @ashrafkhamis /doc/integration/glab/ @aqualls -/doc/integration/gmail_action_buttons_for_gitlab.md @ashrafkhamis -/doc/integration/index.md @ashrafkhamis -/doc/integration/jenkins.md @ashrafkhamis -/doc/integration/jira/ @ashrafkhamis +/doc/integration/gmail_action_buttons_for_gitlab.md @eread @ashrafkhamis +/doc/integration/index.md @eread @ashrafkhamis +/doc/integration/jenkins.md @eread @ashrafkhamis +/doc/integration/jira/ @eread @ashrafkhamis /doc/integration/mattermost/ @axil /doc/integration/partner_marketplace.md @fneill /doc/integration/recaptcha.md @phillipwells /doc/integration/security_partners/ @rdickenson -/doc/integration/slash_commands.md @ashrafkhamis +/doc/integration/slash_commands.md @eread @ashrafkhamis /doc/integration/sourcegraph.md @aqualls -/doc/integration/trello_power_up.md @ashrafkhamis +/doc/integration/trello_power_up.md @eread @ashrafkhamis /doc/integration/vault.md @phillipwells /doc/operations/error_tracking.md @drcatherinepope /doc/operations/feature_flags.md @phillipwells @@ -811,21 +812,23 @@ lib/gitlab/checks/** /doc/subscriptions/gitlab_dedicated/ @drcatherinepope /doc/topics/authentication/ @jglassman1 /doc/topics/autodevops/ @phillipwells -/doc/topics/awesome_co.md @sselhorn +/doc/topics/data_seeder.md @sselhorn /doc/topics/git/ @aqualls /doc/topics/gitlab_flow.md @aqualls /doc/topics/offline/ @axil /doc/topics/plan_and_track.md @msedlakjakubowski -/doc/topics/your_work.md @sselhorn /doc/tutorials/ @kpaizee -/doc/tutorials/create_compliance_pipeline.md @eread -/doc/tutorials/fuzz_testing_tutorial.md @rdickenson -/doc/tutorials/scan_result_policy.md @rdickenson +/doc/tutorials/boards_for_teams/ @msedlakjakubowski +/doc/tutorials/compliance_pipeline/ @eread +/doc/tutorials/convert_personal_namespace_to_group/ @lciutacu +/doc/tutorials/fuzz_testing/ @rdickenson +/doc/tutorials/move_personal_project_to_group/ @lciutacu +/doc/tutorials/scan_result_policy/ @rdickenson /doc/update/ @axil /doc/update/background_migrations.md @aqualls /doc/user/admin_area/analytics/ @lciutacu /doc/user/admin_area/credentials_inventory.md @jglassman1 -/doc/user/admin_area/custom_project_templates.md @eread +/doc/user/admin_area/custom_project_templates.md @aqualls /doc/user/admin_area/diff_limits.md @aqualls /doc/user/admin_area/external_users.md @jglassman1 /doc/user/admin_area/geo_sites.md @axil @@ -846,12 +849,12 @@ lib/gitlab/checks/** /doc/user/admin_area/settings/files_api_rate_limits.md @aqualls /doc/user/admin_area/settings/git_lfs_rate_limits.md @aqualls /doc/user/admin_area/settings/gitaly_timeouts.md @eread -/doc/user/admin_area/settings/import_export_rate_limits.md @eread +/doc/user/admin_area/settings/import_export_rate_limits.md @eread @ashrafkhamis /doc/user/admin_area/settings/incident_management_rate_limits.md @msedlakjakubowski /doc/user/admin_area/settings/index.md @aqualls /doc/user/admin_area/settings/instance_template_repository.md @aqualls /doc/user/admin_area/settings/package_registry_rate_limits.md @marcel.amirault -/doc/user/admin_area/settings/project_integration_management.md @ashrafkhamis +/doc/user/admin_area/settings/project_integration_management.md @eread @ashrafkhamis /doc/user/admin_area/settings/push_event_activities_limit.md @aqualls /doc/user/admin_area/settings/rate_limit_on_issues_creation.md @msedlakjakubowski /doc/user/admin_area/settings/rate_limit_on_notes_creation.md @msedlakjakubowski @@ -881,10 +884,10 @@ lib/gitlab/checks/** /doc/user/group/clusters/ @phillipwells /doc/user/group/compliance_frameworks.md @eread /doc/user/group/contribution_analytics/ @lciutacu -/doc/user/group/custom_project_templates.md @eread +/doc/user/group/custom_project_templates.md @aqualls /doc/user/group/devops_adoption/ @lciutacu /doc/user/group/epics/ @msedlakjakubowski -/doc/user/group/import/ @eread +/doc/user/group/import/ @eread @ashrafkhamis /doc/user/group/index.md @lciutacu /doc/user/group/insights/ @lciutacu /doc/user/group/issues_analytics/ @msedlakjakubowski @@ -930,16 +933,16 @@ lib/gitlab/checks/** /doc/user/project/file_lock.md @aqualls /doc/user/project/git_attributes.md @aqualls /doc/user/project/highlighting.md @aqualls -/doc/user/project/import/ @eread +/doc/user/project/import/ @eread @ashrafkhamis /doc/user/project/import/jira.md @msedlakjakubowski /doc/user/project/index.md @lciutacu /doc/user/project/insights/ @lciutacu -/doc/user/project/integrations/ @ashrafkhamis +/doc/user/project/integrations/ @eread @ashrafkhamis /doc/user/project/integrations/prometheus.md @msedlakjakubowski /doc/user/project/integrations/prometheus_library/ @msedlakjakubowski /doc/user/project/issue_board.md @msedlakjakubowski /doc/user/project/issues/ @msedlakjakubowski -/doc/user/project/issues/csv_import.md @eread +/doc/user/project/issues/csv_import.md @eread @ashrafkhamis /doc/user/project/labels.md @msedlakjakubowski /doc/user/project/members/ @lciutacu /doc/user/project/merge_requests/ @aqualls @@ -962,14 +965,13 @@ lib/gitlab/checks/** /doc/user/project/repository/web_editor.md @ashrafkhamis /doc/user/project/requirements/ @msedlakjakubowski /doc/user/project/service_desk.md @msedlakjakubowski -/doc/user/project/settings/import_export.md @eread -/doc/user/project/settings/import_export_troubleshooting.md @eread +/doc/user/project/settings/import_export.md @eread @ashrafkhamis +/doc/user/project/settings/import_export_troubleshooting.md @eread @ashrafkhamis /doc/user/project/settings/index.md @lciutacu /doc/user/project/settings/project_access_tokens.md @jglassman1 /doc/user/project/system_notes.md @aqualls /doc/user/project/time_tracking.md @msedlakjakubowski /doc/user/project/web_ide/ @ashrafkhamis -/doc/user/project/web_ide_beta/ @ashrafkhamis /doc/user/project/working_with_projects.md @lciutacu /doc/user/public_access.md @lciutacu /doc/user/report_abuse.md @phillipwells @@ -981,7 +983,6 @@ lib/gitlab/checks/** /doc/user/tasks.md @msedlakjakubowski /doc/user/todos.md @msedlakjakubowski /doc/user/usage_quotas.md @fneill -/doc/user/workspace/quick_start/ @ashrafkhamis # End rake-managed-docs-block [Authentication and Authorization] @gitlab-org/manage/authentication-and-authorization/approvers diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index e61eb0b5ca6..a53c195e025 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -132,11 +132,15 @@ trigger-omnibus-env: echo "EE=$([[ $FOSS_ONLY == '1' ]] && echo 'false' || echo 'true')" >> $BUILD_ENV target_branch_name="${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-${CI_COMMIT_REF_NAME}}" echo "TRIGGER_BRANCH=$([[ "${target_branch_name}" =~ ^[0-9-]+-stable(-ee)?$ ]] && echo ${target_branch_name%-ee} || echo 'master')" >> $BUILD_ENV + - | echo "Built environment file for omnibus build:" cat $BUILD_ENV artifacts: + expire_in: 3 days reports: dotenv: $BUILD_ENV + paths: + - $BUILD_ENV trigger-omnibus-env-ce: extends: diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml index 94492e93b75..aa61e67de08 100644 --- a/.gitlab/ci/review-apps/main.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml @@ -90,9 +90,9 @@ review-build-cng: strategy: depend .review-workflow-base: - extends: - - .default-retry image: ${REVIEW_APPS_IMAGE} + retry: + max: 2 # This is confusing but this means "3 runs at max" variables: HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}" DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}" @@ -126,6 +126,7 @@ review-deploy: - *base-before_script - !reference [".use-kube-context", before_script] script: + - run_timed_command "retry delete_helm_release" - run_timed_command "check_kube_domain" - run_timed_command "download_chart" - run_timed_command "deploy" || (display_deployment_debug && exit 1) diff --git a/.gitlab/issue_templates/Feature Flag Cleanup.md b/.gitlab/issue_templates/Feature Flag Cleanup.md index d32b0c874d4..f96165fd359 100644 --- a/.gitlab/issue_templates/Feature Flag Cleanup.md +++ b/.gitlab/issue_templates/Feature Flag Cleanup.md @@ -48,4 +48,4 @@ Are there any other stages or teams involved that need to be kept in the loop? - [ ] Close this rollout issue. -/label ~"feature flag" ~"type::feature" ~"feature::addition" +/label ~"feature flag" ~"type::maintenance" ~"maintenance::removal" diff --git a/.rubocop.yml b/.rubocop.yml index b3f24e12c22..81ad4cd31f3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -527,6 +527,12 @@ RSpec/AvoidTestProf: - 'ee/spec/lib/gitlab/background_migration/**/*.rb' - 'ee/spec/lib/ee/gitlab/background_migration/**/*.rb' +RSpec/AvoidConditionalStatements: + Enabled: true + Include: + - 'spec/features/**/*.rb' + - 'ee/spec/features/**/*.rb' + RSpec/FactoriesInMigrationSpecs: Enabled: true Include: diff --git a/.rubocop_todo/rspec/avoid_conditional_statements.yml b/.rubocop_todo/rspec/avoid_conditional_statements.yml new file mode 100644 index 00000000000..43ffaaa452a --- /dev/null +++ b/.rubocop_todo/rspec/avoid_conditional_statements.yml @@ -0,0 +1,84 @@ +--- +RSpec/AvoidConditionalStatements: + Details: grace period + Exclude: + - 'ee/spec/features/admin/admin_settings_spec.rb' + - 'ee/spec/features/analytics/code_analytics_spec.rb' + - 'ee/spec/features/billings/billing_plans_spec.rb' + - 'ee/spec/features/boards/scoped_issue_board_spec.rb' + - 'ee/spec/features/boards/user_visits_board_spec.rb' + - 'ee/spec/features/ci_shared_runner_warnings_spec.rb' + - 'ee/spec/features/epic_boards/epic_boards_spec.rb' + - 'ee/spec/features/epics/epic_show_spec.rb' + - 'ee/spec/features/epics/gfm_autocomplete_spec.rb' + - 'ee/spec/features/group_protected_branches_spec.rb' + - 'ee/spec/features/groups/analytics/cycle_analytics/filters_and_data_spec.rb' + - 'ee/spec/features/groups/analytics/cycle_analytics/multiple_value_streams_spec.rb' + - 'ee/spec/features/groups/iterations/user_views_iteration_spec.rb' + - 'ee/spec/features/incidents/incident_details_spec.rb' + - 'ee/spec/features/issues/user_sees_empty_state_spec.rb' + - 'ee/spec/features/labels_hierarchy_spec.rb' + - 'ee/spec/features/profiles/usage_quotas_spec.rb' + - 'ee/spec/features/projects/analytics/visualization_designer_spec.rb' + - 'ee/spec/features/projects/licenses/maintainer_views_policies_spec.rb' + - 'ee/spec/features/projects/merge_requests/user_approves_merge_request_spec.rb' + - 'ee/spec/features/projects/settings/issues_settings_spec.rb' + - 'ee/spec/features/projects_spec.rb' + - 'ee/spec/features/registrations/email_confirmation_spec.rb' + - 'ee/spec/features/registrations/identity_verification_spec.rb' + - 'ee/spec/features/search/elastic/snippet_search_spec.rb' + - 'ee/spec/features/subscriptions/expiring_subscription_message_spec.rb' + - 'ee/spec/features/users/identity_verification_spec.rb' + - 'spec/features/admin/dashboard_spec.rb' + - 'spec/features/calendar_spec.rb' + - 'spec/features/groups/dependency_proxy_for_containers_spec.rb' + - 'spec/features/groups/empty_states_spec.rb' + - 'spec/features/groups/group_settings_spec.rb' + - 'spec/features/groups/members/sort_members_spec.rb' + - 'spec/features/groups/navbar_spec.rb' + - 'spec/features/issuables/issuable_list_spec.rb' + - 'spec/features/issuables/markdown_references/jira_spec.rb' + - 'spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb' + - 'spec/features/issues/user_bulk_edits_issues_labels_spec.rb' + - 'spec/features/issues/user_creates_branch_and_merge_request_spec.rb' + - 'spec/features/issues/user_edits_issue_spec.rb' + - 'spec/features/issues/user_interacts_with_awards_spec.rb' + - 'spec/features/labels_hierarchy_spec.rb' + - 'spec/features/markdown/keyboard_shortcuts_spec.rb' + - 'spec/features/merge_request/batch_comments_spec.rb' + - 'spec/features/merge_request/user_posts_diff_notes_spec.rb' + - 'spec/features/merge_request/user_reverts_merge_request_spec.rb' + - 'spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb' + - 'spec/features/merge_request/user_squashes_merge_request_spec.rb' + - 'spec/features/merge_request/user_suggests_changes_on_diff_spec.rb' + - 'spec/features/monitor_sidebar_link_spec.rb' + - 'spec/features/oauth_login_spec.rb' + - 'spec/features/participants_autocomplete_spec.rb' + - 'spec/features/profiles/user_edit_profile_spec.rb' + - 'spec/features/projects/blobs/edit_spec.rb' + - 'spec/features/projects/branches_spec.rb' + - 'spec/features/projects/commit/cherry_pick_spec.rb' + - 'spec/features/projects/commit/user_reverts_commit_spec.rb' + - 'spec/features/projects/compare_spec.rb' + - 'spec/features/projects/deploy_keys_spec.rb' + - 'spec/features/projects/environments/environment_spec.rb' + - 'spec/features/projects/files/template_selector_menu_spec.rb' + - 'spec/features/projects/integrations/user_activates_issue_tracker_spec.rb' + - 'spec/features/projects/integrations/user_activates_jira_spec.rb' + - 'spec/features/projects/labels/user_removes_labels_spec.rb' + - 'spec/features/projects/members/sorting_spec.rb' + - 'spec/features/projects/milestones/milestone_spec.rb' + - 'spec/features/projects/releases/user_views_releases_spec.rb' + - 'spec/features/projects/settings/project_settings_spec.rb' + - 'spec/features/projects/settings/repository_settings_spec.rb' + - 'spec/features/projects/settings/user_transfers_a_project_spec.rb' + - 'spec/features/projects/show/user_sees_git_instructions_spec.rb' + - 'spec/features/projects/tree/create_directory_spec.rb' + - 'spec/features/projects/tree/create_file_spec.rb' + - 'spec/features/projects_spec.rb' + - 'spec/features/search/user_uses_header_search_field_spec.rb' + - 'spec/features/snippets/explore_spec.rb' + - 'spec/features/tags/developer_creates_tag_spec.rb' + - 'spec/features/usage_stats_consent_spec.rb' + - 'spec/features/users/login_spec.rb' + - 'spec/features/users/overview_spec.rb' diff --git a/app/assets/images/vulnerability/secureflag-logo.svg b/app/assets/images/vulnerability/secureflag-logo.svg new file mode 100644 index 00000000000..621c56b9043 --- /dev/null +++ b/app/assets/images/vulnerability/secureflag-logo.svg @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg" xmlns:bx="https://boxy-svg.com"> + <defs> + <linearGradient id="paint1_linear_117_388" x1="8.32922" y1="0.701083" x2="25.6103" y2="8.8381" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(5.965987, 0, 0, 5.965987, -1.949537, -0.014549)"> + <stop stop-color="#005AEC" /> + <stop offset="1" stop-color="#12DFE7" /> + </linearGradient> + <linearGradient id="paint2_linear_117_388" x1="3.30485" y1="11.2131" x2="20.4972" y2="19.4227" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(5.965987, 0, 0, 5.965987, -1.949537, -0.014549)"> + <stop stop-color="#005AEC" /> + <stop offset="1" stop-color="#12DFE7" /> + </linearGradient> + </defs> + <g style="" transform="matrix(3.098345, 0, 0, 3.098345, 46.765705, 8.335629)" + bx:origin="0.503455 0.502894"> + <path d="M 65.436 0.003 L 65.436 26.662 L 87.144 12.772 L 65.436 0.003 Z" + fill="url(#paint1_linear_117_388)" style="" /> + <path + d="M 108.686 77.337 L 87.143 65.001 L 87.143 38.543 L 65.434 51.815 L 43.562 38.543 L 43.809 64.746 L 22.512 77.592 L 0.393 64.321 L 0.393 116.301 L 65.352 155.095 L 129.901 116.301 L 129.901 64.406 L 108.686 77.337 Z M 65.434 103.2 L 43.562 90.609 L 43.562 114.344 L 22.923 102.945 L 43.562 90.609 L 65.434 77.848 C 65.434 77.848 85.663 90.694 86.156 90.609 C 86.65 90.523 65.434 103.2 65.434 103.2 Z M 86.156 114.344 L 86.156 90.609 L 106.795 102.945 L 86.156 114.344 Z" + fill="url(#paint2_linear_117_388)" style="" /> + </g> +</svg>
\ No newline at end of file diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index 04b3599ea8c..39a7a76e91f 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -39,7 +39,7 @@ export function renderGFM(element) { '.js-render-mermaid', '[lang="json"][data-lang-params="table"]', '.gfm-project_member', - '.gfm-issue, .gfm-merge_request', + '.gfm-issue, .gfm-work_item, .gfm-merge_request', '.js-render-metrics', '.js-render-observability', ].map((selector) => Array.from(element.querySelectorAll(selector))); @@ -50,7 +50,9 @@ export function renderGFM(element) { renderSandboxedMermaid(mermaidEls); renderJSONTable(tableEls.map((e) => e.parentNode)); highlightCurrentUser(userEls); - renderMetrics(metricsEls); + if (!window.gon?.features?.removeMonitorMetrics) { + renderMetrics(metricsEls); + } renderObservability(observabilityEls); initPopovers(popoverEls); } diff --git a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue index 2cff11c1aa1..087ddafb137 100644 --- a/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue +++ b/app/assets/javascripts/ci/runner/components/runner_list_empty_state.vue @@ -43,8 +43,12 @@ export default { }, computed: { shouldShowCreateRunnerWorkflow() { - // create_runner_workflow_for_admin feature flag - return this.newRunnerPath && this.glFeatures?.createRunnerWorkflowForAdmin; + // create_runner_workflow_for_admin or create_runner_workflow_for_namespace + return ( + this.newRunnerPath && + (this.glFeatures?.createRunnerWorkflowForAdmin || + this.glFeatures?.createRunnerWorkflowForNamespace) + ); }, }, modalId: 'runners-empty-state-instructions-modal', diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue index 9f3e6f247d7..a4ca44b8e5a 100644 --- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue @@ -270,6 +270,7 @@ export default { v-if="noRunnersFound" :registration-token="registrationToken" :is-search-filtered="isSearchFiltered" + :new-runner-path="newRunnerPath" :svg-path="emptyStateSvgPath" :filtered-svg-path="emptyStateFilteredSvgPath" /> diff --git a/app/assets/javascripts/issuable/popover/index.js b/app/assets/javascripts/issuable/popover/index.js index de3c8160b7a..9430419685b 100644 --- a/app/assets/javascripts/issuable/popover/index.js +++ b/app/assets/javascripts/issuable/popover/index.js @@ -6,6 +6,7 @@ import MRPopover from './components/mr_popover.vue'; const componentsByReferenceType = { issue: IssuePopover, + work_item: IssuePopover, merge_request: MRPopover, }; diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue index 0d40a727029..25c06aa2f7f 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/ml_experiments_show.vue @@ -86,6 +86,7 @@ export default { { key: 'user', label: this.$options.i18n.USER_LABEL }, ...this.paramNames, ...this.metricNames, + { key: 'ci_job', label: this.$options.i18n.CI_JOB_LABEL }, { key: 'artifact', label: this.$options.i18n.ARTIFACTS_LABEL }, ]; }, @@ -226,6 +227,15 @@ export default { <gl-link v-if="data.value" :href="data.value.path">@{{ data.value.username }}</gl-link> <div v-else>{{ $options.i18n.NO_DATA_CONTENT }}</div> </template> + + <template #cell(ci_job)="data"> + <gl-link v-if="data.value" :href="data.value.path" target="_blank">{{ + data.value.name + }}</gl-link> + <div v-else class="gl-font-style-italic gl-text-gray-500"> + {{ $options.i18n.NO_JOB }} + </div> + </template> </gl-table-lite> </div> diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/translations.js b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/translations.js index 604658ded3d..3af33f53fbd 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/translations.js +++ b/app/assets/javascripts/ml/experiment_tracking/routes/experiments/show/translations.js @@ -3,12 +3,14 @@ import { s__ } from '~/locale'; export const ARTIFACTS_LABEL = s__('MlExperimentTracking|Artifacts'); export const DETAILS_LABEL = s__('MlExperimentTracking|Details'); export const USER_LABEL = s__('MlExperimentTracking|Author'); +export const CI_JOB_LABEL = s__('MlExperimentTracking|CI Job'); export const CREATED_AT_LABEL = s__('MlExperimentTracking|Created at'); export const NAME_LABEL = s__('MlExperimentTracking|Name'); export const NO_DATA_CONTENT = s__('MlExperimentTracking|-'); export const FILTER_CANDIDATES_LABEL = s__('MlExperimentTracking|Filter candidates'); export const NO_CANDIDATE_NAME = s__('MlExperimentTracking|No name'); export const NO_ARTIFACT = s__('MlExperimentTracking|No artifacts'); +export const NO_JOB = s__('MlExperimentTracking|-'); export const CREATE_NEW_LABEL = s__('MlExperimentTracking|Create new candidates'); export const EMPTY_STATE_DESCRIPTION_LABEL = s__( 'MlExperimentTracking|No candidates logged for the query. Create new candidates using the MLflow client.', diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js index 3bf0401ef5e..1b86d7d0a2b 100644 --- a/app/assets/javascripts/security_configuration/components/constants.js +++ b/app/assets/javascripts/security_configuration/components/constants.js @@ -17,6 +17,7 @@ import { import kontraLogo from 'images/vulnerability/kontra-logo.svg'; import scwLogo from 'images/vulnerability/scw-logo.svg'; +import secureflagLogo from 'images/vulnerability/secureflag-logo.svg'; import configureSastMutation from '../graphql/configure_sast.mutation.graphql'; import configureSastIacMutation from '../graphql/configure_iac.mutation.graphql'; import configureSecretDetectionMutation from '../graphql/configure_secret_detection.mutation.graphql'; @@ -313,6 +314,9 @@ export const TEMP_PROVIDER_LOGOS = { [__('Secure Code Warrior')]: { svg: scwLogo, }, + SecureFlag: { + svg: secureflagLogo, + }, }; // Use the `url` field from the GraphQL query once this issue is resolved @@ -320,4 +324,5 @@ export const TEMP_PROVIDER_LOGOS = { export const TEMP_PROVIDER_URLS = { Kontra: 'https://application.security/', [__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/', + SecureFlag: 'https://www.secureflag.com/', }; diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue index 32ac5daf5de..f9f4bf260a1 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue @@ -21,19 +21,16 @@ export default { WorkItemCommentForm, }, mixins: [Tracking.mixin()], + inject: ['fullPath'], props: { workItemId: { type: String, required: true, }, - fullPath: { + workItemIid: { type: String, required: true, }, - queryVariables: { - type: Object, - required: true, - }, discussionId: { type: String, required: false, @@ -85,13 +82,16 @@ export default { workItem: { query: workItemByIidQuery, variables() { - return this.queryVariables; + return { + fullPath: this.fullPath, + iid: this.workItemIid, + }; }, update(data) { return data.workspace.workItems.nodes[0]; }, skip() { - return !this.queryVariables.iid; + return !this.workItemIid; }, error() { this.$emit('error', i18n.fetchError); diff --git a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue index e40c6296229..e98e03f76fd 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_discussion.vue @@ -18,16 +18,13 @@ export default { DiscussionNotesRepliesWrapper, WorkItemNoteReplying, }, + inject: ['fullPath'], props: { workItemId: { type: String, required: true, }, - queryVariables: { - type: Object, - required: true, - }, - fullPath: { + workItemIid: { type: String, required: true, }, @@ -161,8 +158,7 @@ export default { :assignees="assignees" :can-set-work-item-metadata="canSetWorkItemMetadata" :work-item-id="workItemId" - :query-variables="queryVariables" - :full-path="fullPath" + :work-item-iid="workItemIid" @startReplying="showReplyForm" @deleteNote="$emit('deleteNote', note)" @reportAbuse="$emit('reportAbuse', note)" @@ -192,9 +188,8 @@ export default { :markdown-preview-path="markdownPreviewPath" :assignees="assignees" :work-item-id="workItemId" + :work-item-iid="workItemIid" :can-set-work-item-metadata="canSetWorkItemMetadata" - :query-variables="queryVariables" - :full-path="fullPath" @startReplying="showReplyForm" @deleteNote="$emit('deleteNote', note)" @reportAbuse="$emit('reportAbuse', note)" @@ -219,9 +214,8 @@ export default { :markdown-preview-path="markdownPreviewPath" :assignees="assignees" :work-item-id="workItemId" + :work-item-iid="workItemIid" :can-set-work-item-metadata="canSetWorkItemMetadata" - :query-variables="queryVariables" - :full-path="fullPath" @startReplying="showReplyForm" @deleteNote="$emit('deleteNote', reply)" @reportAbuse="$emit('reportAbuse', reply)" @@ -233,9 +227,8 @@ export default { v-if="shouldShowReplyForm" :notes-form="false" :autofocus="autofocus" - :query-variables="queryVariables" - :full-path="fullPath" :work-item-id="workItemId" + :work-item-iid="workItemIid" :discussion-id="discussionId" :work-item-type="workItemType" :sort-order="sortOrder" diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_note.vue index a6ab8a371de..75b0970a89e 100644 --- a/app/assets/javascripts/work_items/components/notes/work_item_note.vue +++ b/app/assets/javascripts/work_items/components/notes/work_item_note.vue @@ -32,16 +32,13 @@ export default { EditedAt, }, mixins: [Tracking.mixin()], + inject: ['fullPath'], props: { - fullPath: { + workItemId: { type: String, required: true, }, - queryVariables: { - type: Object, - required: true, - }, - workItemId: { + workItemIid: { type: String, required: true, }, @@ -158,13 +155,16 @@ export default { workItem: { query: workItemByIidQuery, variables() { - return this.queryVariables; + return { + fullPath: this.fullPath, + iid: this.workItemIid, + }; }, update(data) { return data.workspace?.workItems?.nodes[0]; }, skip() { - return !this.queryVariables.iid; + return !this.workItemIid; }, error() { this.$emit('error', i18n.fetchError); diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue index 95527dda1d4..4e6583b65f8 100644 --- a/app/assets/javascripts/work_items/components/work_item_assignees.vue +++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue @@ -54,6 +54,7 @@ export default { GlIntersectionObserver, }, mixins: [Tracking.mixin()], + inject: ['fullPath'], props: { workItemId: { type: String, @@ -81,10 +82,6 @@ export default { required: false, default: false, }, - fullPath: { - type: String, - required: true, - }, }, data() { return { diff --git a/app/assets/javascripts/work_items/components/work_item_created_updated.vue b/app/assets/javascripts/work_items/components/work_item_created_updated.vue index 5c30e984f13..78a86aa49a4 100644 --- a/app/assets/javascripts/work_items/components/work_item_created_updated.vue +++ b/app/assets/javascripts/work_items/components/work_item_created_updated.vue @@ -10,17 +10,13 @@ export default { GlSprintf, TimeAgoTooltip, }, + inject: ['fullPath'], props: { workItemIid: { type: String, required: false, default: null, }, - fullPath: { - type: String, - required: false, - default: null, - }, }, computed: { createdAt() { diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue index 942f5d4a9f0..f3c94732aae 100644 --- a/app/assets/javascripts/work_items/components/work_item_description.vue +++ b/app/assets/javascripts/work_items/components/work_item_description.vue @@ -28,19 +28,16 @@ export default { WorkItemDescriptionRendered, }, mixins: [glFeatureFlagMixin(), Tracking.mixin()], + inject: ['fullPath'], props: { workItemId: { type: String, required: true, }, - fullPath: { + workItemIid: { type: String, required: true, }, - queryVariables: { - type: Object, - required: true, - }, }, markdownDocsPath: helpPagePath('user/project/quick_actions'), quickActionsDocsPath: helpPagePath('user/project/quick_actions'), @@ -64,13 +61,16 @@ export default { workItem: { query: workItemByIidQuery, variables() { - return this.queryVariables; + return { + fullPath: this.fullPath, + iid: this.workItemIid, + }; }, update(data) { return data.workspace.workItems.nodes[0]; }, skip() { - return !this.queryVariables.iid; + return !this.workItemIid; }, result() { if (this.isEditing) { diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue index ef99001c0e8..56802f76bba 100644 --- a/app/assets/javascripts/work_items/components/work_item_detail.vue +++ b/app/assets/javascripts/work_items/components/work_item_detail.vue @@ -140,7 +140,10 @@ export default { workItem: { query: workItemByIidQuery, variables() { - return this.queryVariables; + return { + fullPath: this.fullPath, + iid: this.workItemIid, + }; }, skip() { return !this.workItemIid; @@ -314,12 +317,6 @@ export default { workItemNotes() { return this.isWidgetPresent(WIDGET_TYPE_NOTES); }, - queryVariables() { - return { - fullPath: this.fullPath, - iid: this.workItemIid, - }; - }, children() { return this.workItem ? findHierarchyWidgetChildren(this.workItem) : []; }, @@ -398,10 +395,12 @@ export default { this.toggleChildFromCache(child, child.id, client); }, toggleChildFromCache(workItem, childId, store) { - const sourceData = store.readQuery({ + const query = { query: workItemByIidQuery, - variables: this.queryVariables, - }); + variables: { fullPath: this.fullPath, iid: this.workItemIid }, + }; + + const sourceData = store.readQuery(query); const newData = produce(sourceData, (draftState) => { const { widgets } = draftState.workspace.workItems.nodes[0]; @@ -416,11 +415,7 @@ export default { } }); - store.writeQuery({ - query: workItemByIidQuery, - variables: this.queryVariables, - data: newData, - }); + store.writeQuery({ ...query, data: newData }); }, async updateWorkItem(workItem, childId, parentId) { return this.$apollo.mutate({ @@ -603,7 +598,7 @@ export default { :can-update="canUpdate" @error="updateError = $event" /> - <work-item-created-updated :work-item-iid="workItemIid" :full-path="fullPath" /> + <work-item-created-updated :work-item-iid="workItemIid" /> <work-item-state :work-item="workItem" :work-item-parent-id="workItemParentId" @@ -618,15 +613,13 @@ export default { :allows-multiple-assignees="workItemAssignees.allowsMultipleAssignees" :work-item-type="workItemType" :can-invite-members="workItemAssignees.canInviteMembers" - :full-path="fullPath" @error="updateError = $event" /> <work-item-labels v-if="workItemLabels" - :work-item-id="workItem.id" :can-update="canUpdate" - :full-path="fullPath" - :query-variables="queryVariables" + :work-item-id="workItem.id" + :work-item-iid="workItem.iid" @error="updateError = $event" /> <work-item-due-date @@ -644,7 +637,6 @@ export default { :work-item-milestone="workItemMilestone.milestone" :work-item-type="workItemType" :can-update="canUpdate" - :full-path="fullPath" @error="updateError = $event" /> <work-item-weight @@ -653,8 +645,8 @@ export default { :can-update="canUpdate" :weight="workItemWeight.weight" :work-item-id="workItem.id" + :work-item-iid="workItem.iid" :work-item-type="workItemType" - :query-variables="queryVariables" @error="updateError = $event" /> <work-item-progress @@ -664,7 +656,6 @@ export default { :progress="workItemProgress.progress" :work-item-id="workItem.id" :work-item-type="workItemType" - :query-variables="queryVariables" @error="updateError = $event" /> <work-item-iteration @@ -673,9 +664,8 @@ export default { :iteration="workItemIteration.iteration" :can-update="canUpdate" :work-item-id="workItem.id" + :work-item-iid="workItem.iid" :work-item-type="workItemType" - :query-variables="queryVariables" - :full-path="fullPath" @error="updateError = $event" /> <work-item-health-status @@ -684,16 +674,14 @@ export default { :health-status="workItemHealthStatus.healthStatus" :can-update="canUpdate" :work-item-id="workItem.id" + :work-item-iid="workItem.iid" :work-item-type="workItemType" - :query-variables="queryVariables" - :full-path="fullPath" @error="updateError = $event" /> <work-item-description v-if="hasDescriptionWidget" :work-item-id="workItem.id" - :full-path="fullPath" - :query-variables="queryVariables" + :work-item-iid="workItem.iid" class="gl-pt-5" @error="updateError = $event" /> @@ -705,7 +693,6 @@ export default { :work-item-iid="workItemIid" :children="children" :can-update="canUpdate" - :project-path="fullPath" :confidential="workItem.confidential" @addWorkItemChild="addChild" @removeChild="removeChild" @@ -715,8 +702,6 @@ export default { v-if="workItemNotes" :work-item-id="workItem.id" :work-item-iid="workItem.iid" - :query-variables="queryVariables" - :full-path="fullPath" :work-item-type="workItemType" :is-modal="isModal" :assignees="workItemAssignees && workItemAssignees.assignees.nodes" diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue index 574ac5f0f5d..015c86ba043 100644 --- a/app/assets/javascripts/work_items/components/work_item_labels.vue +++ b/app/assets/javascripts/work_items/components/work_item_labels.vue @@ -43,21 +43,18 @@ export default { LabelItem, }, mixins: [Tracking.mixin()], + inject: ['fullPath'], props: { workItemId: { type: String, required: true, }, - canUpdate: { - type: Boolean, - required: true, - }, - fullPath: { + workItemIid: { type: String, required: true, }, - queryVariables: { - type: Object, + canUpdate: { + type: Boolean, required: true, }, }, @@ -76,13 +73,16 @@ export default { workItem: { query: workItemByIidQuery, variables() { - return this.queryVariables; + return { + fullPath: this.fullPath, + iid: this.workItemIid, + }; }, update(data) { return data.workspace.workItems.nodes[0]; }, skip() { - return !this.queryVariables.iid; + return !this.workItemIid; }, error() { this.$emit('error', i18n.fetchError); diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js index 45dd4c00683..636c9357170 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/index.js +++ b/app/assets/javascripts/work_items/components/work_item_links/index.js @@ -9,11 +9,11 @@ export default function initWorkItemLinks() { const workItemLinksRoot = document.querySelector('.js-work-item-links-root'); if (!workItemLinksRoot) { - return; + return null; } const { - projectPath, + fullPath, wiHasIssueWeightsFeature, wiHasIterationsFeature, wiHasIssuableHealthStatusFeature, @@ -22,8 +22,7 @@ export default function initWorkItemLinks() { wiReportAbusePath, } = workItemLinksRoot.dataset; - // eslint-disable-next-line no-new - new Vue({ + return new Vue({ el: workItemLinksRoot, name: 'WorkItemLinksRoot', apolloProvider, @@ -31,8 +30,7 @@ export default function initWorkItemLinks() { WorkItemLinks, }, provide: { - projectPath, - fullPath: projectPath, + fullPath, hasIssueWeightsFeature: wiHasIssueWeightsFeature, hasIterationsFeature: wiHasIterationsFeature, hasIssuableHealthStatusFeature: wiHasIssuableHealthStatusFeature, diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue index 2811ebc4bb0..098917f2b56 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue @@ -19,6 +19,7 @@ export default { WorkItemLinkChild, }, mixins: [glFeatureFlagsMixin()], + inject: ['fullPath'], props: { workItemType: { type: String, @@ -43,10 +44,6 @@ export default { required: false, default: false, }, - projectPath: { - type: String, - required: true, - }, fetchByIid: { type: Boolean, required: false, @@ -86,7 +83,7 @@ export default { queryVariables() { return this.fetchByIid ? { - fullPath: this.projectPath, + fullPath: this.fullPath, iid: this.workItemIid, } : { @@ -98,7 +95,7 @@ export default { addWorkItemQuery({ id, iid }) { const variables = this.fetchByIid ? { - fullPath: this.projectPath, + fullPath: this.fullPath, iid, } : { @@ -232,7 +229,6 @@ export default { <work-item-link-child v-for="child in children" :key="child.id" - :project-path="projectPath" :can-update="canUpdate" :issuable-gid="workItemId" :child-item="child" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue index 35430261e29..8152412a5c1 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue @@ -38,11 +38,8 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + inject: ['fullPath'], props: { - projectPath: { - type: String, - required: true, - }, canUpdate: { type: Boolean, required: true, @@ -121,9 +118,7 @@ export default { return this.isItemOpen ? __('Created') : __('Closed'); }, childPath() { - return `${gon?.relative_url_root || ''}/${this.projectPath}/-/work_items/${ - this.childItem.iid - }`; + return `${gon?.relative_url_root || ''}/${this.fullPath}/-/work_items/${this.childItem.iid}`; }, chevronType() { return this.isExpanded ? 'chevron-down' : 'chevron-right'; @@ -353,7 +348,6 @@ export default { </div> <work-item-tree-children v-if="isExpanded" - :project-path="projectPath" :can-update="canUpdate" :work-item-id="issuableGid" :work-item-type="workItemType" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue index 7feef26f211..46c109f2d57 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue @@ -36,7 +36,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - inject: ['projectPath', 'reportAbusePath'], + inject: ['fullPath', 'reportAbusePath'], props: { issuableId: { type: Number, @@ -226,7 +226,7 @@ export default { this.$apollo.addSmartQuery('prefetchedWorkItem', { query: workItemByIidQuery, variables: { - fullPath: this.projectPath, + fullPath: this.fullPath, iid, }, update(data) { @@ -335,7 +335,6 @@ export default { /> <work-item-children-wrapper :children="children" - :project-path="projectPath" :can-update="canUpdate" :work-item-id="issuableGid" @removeChild="removeChild" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue index af475496075..51c83784d06 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue @@ -41,7 +41,7 @@ export default { GlFormCheckbox, GlTooltip, }, - inject: ['projectPath', 'hasIterationsFeature'], + inject: ['fullPath', 'hasIterationsFeature'], props: { issuableGid: { type: String, @@ -88,7 +88,7 @@ export default { query: projectWorkItemTypesQuery, variables() { return { - fullPath: this.projectPath, + fullPath: this.fullPath, }; }, update(data) { @@ -99,7 +99,7 @@ export default { query: projectWorkItemsQuery, variables() { return { - projectPath: this.projectPath, + fullPath: this.fullPath, searchTerm: this.search?.title || this.search, types: [this.childrenType], in: this.search ? 'TITLE' : undefined, @@ -131,7 +131,7 @@ export default { workItemInput() { let workItemInput = { title: this.search?.title || this.search, - projectPath: this.projectPath, + projectPath: this.fullPath, workItemTypeId: this.childWorkItemType, hierarchyWidget: { parentId: this.issuableGid, diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index 4dcc4d51957..cbca78e4b14 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -23,6 +23,7 @@ export default { WorkItemLinksForm, WorkItemChildrenWrapper, }, + inject: ['fullPath'], props: { workItemType: { type: String, @@ -57,10 +58,6 @@ export default { required: false, default: false, }, - projectPath: { - type: String, - required: true, - }, }, data() { return { @@ -106,7 +103,7 @@ export default { this.$apollo.addSmartQuery('prefetchedWorkItem', { query: workItemByIidQuery, variables: { - fullPath: this.projectPath, + fullPath: this.fullPath, iid, }, update(data) { @@ -164,7 +161,6 @@ export default { /> <work-item-children-wrapper :children="children" - :project-path="projectPath" :can-update="canUpdate" :work-item-id="workItemId" :work-item-iid="workItemIid" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue index ba5c0794395..121c987da71 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue @@ -3,6 +3,7 @@ export default { components: { WorkItemLinkChild: () => import('./work_item_link_child.vue'), }, + inject: ['fullPath'], props: { workItemType: { type: String, @@ -22,10 +23,6 @@ export default { required: false, default: false, }, - projectPath: { - type: String, - required: true, - }, }, }; </script> @@ -35,7 +32,6 @@ export default { <work-item-link-child v-for="child in children" :key="child.id" - :project-path="projectPath" :can-update="canUpdate" :issuable-gid="workItemId" :child-item="child" diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue index 06051eee3a3..693397686d0 100644 --- a/app/assets/javascripts/work_items/components/work_item_milestone.vue +++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue @@ -46,6 +46,7 @@ export default { GlDropdownText, }, mixins: [Tracking.mixin()], + inject: ['fullPath'], props: { workItemId: { type: String, @@ -66,10 +67,6 @@ export default { required: false, default: false, }, - fullPath: { - type: String, - required: true, - }, }, data() { return { diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue index 42afde76a00..092b90a5731 100644 --- a/app/assets/javascripts/work_items/components/work_item_notes.vue +++ b/app/assets/javascripts/work_items/components/work_item_notes.vue @@ -45,6 +45,7 @@ export default { WorkItemNotesActivityHeader, WorkItemHistoryOnlyFilterNote, }, + inject: ['fullPath'], props: { workItemId: { type: String, @@ -54,14 +55,6 @@ export default { type: String, required: true, }, - queryVariables: { - type: Object, - required: true, - }, - fullPath: { - type: String, - required: true, - }, workItemType: { type: String, required: true, @@ -122,9 +115,9 @@ export default { }, workItemCommentFormProps() { return { - queryVariables: this.queryVariables, fullPath: this.fullPath, workItemId: this.workItemId, + workItemIid: this.workItemIid, workItemType: this.workItemType, sortOrder: this.sortOrder, isNewDiscussion: true, @@ -169,7 +162,8 @@ export default { }, variables() { return { - ...this.queryVariables, + fullPath: this.fullPath, + iid: this.workItemIid, after: this.after, pageSize: DEFAULT_PAGE_SIZE_NOTES, }; @@ -179,7 +173,7 @@ export default { return widgets?.find((widget) => widget.type === 'NOTES')?.discussions || []; }, skip() { - return !this.queryVariables.iid; + return !this.workItemIid; }, error() { this.$emit('error', i18n.fetchError); @@ -264,7 +258,8 @@ export default { await this.$apollo.queries.workItemNotes .fetchMore({ variables: { - ...this.queryVariables, + fullPath: this.fullPath, + iid: this.workItemIid, after: this.pageInfo?.endCursor, }, }) @@ -359,9 +354,8 @@ export default { <work-item-discussion :key="getDiscussionKey(discussion)" :discussion="discussion.notes.nodes" - :query-variables="queryVariables" - :full-path="fullPath" :work-item-id="workItemId" + :work-item-iid="workItemIid" :work-item-type="workItemType" :is-modal="isModal" :autocomplete-data-sources="autocompleteDataSources" diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql index fce10f6f2a6..7d63af448d4 100644 --- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql +++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql @@ -1,10 +1,10 @@ query projectWorkItems( $searchTerm: String - $projectPath: ID! + $fullPath: ID! $types: [IssueType!] $in: [IssuableSearchableField!] ) { - workspace: project(fullPath: $projectPath) { + workspace: project(fullPath: $fullPath) { id workItems(search: $searchTerm, types: $types, in: $in) { nodes { diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js index eb37336bbf9..70bda7d3783 100644 --- a/app/assets/javascripts/work_items/index.js +++ b/app/assets/javascripts/work_items/index.js @@ -29,7 +29,6 @@ export const initWorkItemsRoot = () => { apolloProvider, provide: { fullPath, - projectPath: fullPath, hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature), hasOkrsFeature: parseBoolean(hasOkrsFeature), issuesListPath, diff --git a/app/helpers/projects/ml/experiments_helper.rb b/app/helpers/projects/ml/experiments_helper.rb index bcf07df0e72..6e5e13ef0b6 100644 --- a/app/helpers/projects/ml/experiments_helper.rb +++ b/app/helpers/projects/ml/experiments_helper.rb @@ -40,6 +40,7 @@ module Projects { **candidate.params.to_h { |p| [p.name, p.value] }, **candidate.latest_metrics.to_h { |m| [m.name, number_with_precision(m.value, precision: 4)] }, + ci_job: job_info(candidate), artifact: link_to_artifact(candidate), details: link_to_details(candidate), name: candidate.name, @@ -92,6 +93,17 @@ module Projects project_ml_candidate_path(candidate.project, candidate.iid) end + def job_info(candidate) + return unless candidate.from_ci? + + build = candidate.ci_build + + { + path: project_job_path(build.project, build), + name: build.name + } + end + def link_to_experiment(project, experiment) project_ml_experiment_path(project, experiment.iid) end diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb index 0265d609e19..0b6075fbeb8 100644 --- a/app/models/concerns/mentionable/reference_regexes.rb +++ b/app/models/concerns/mentionable/reference_regexes.rb @@ -20,7 +20,7 @@ module Mentionable def self.default_pattern strong_memoize(:default_pattern) do issue_pattern = Issue.reference_pattern - link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic, Vulnerability].map(&:link_reference_pattern).compact) + link_patterns = Regexp.union([Issue, WorkItem, Commit, MergeRequest, Epic, Vulnerability].map(&:link_reference_pattern).compact) reference_pattern(link_patterns, issue_pattern) end end diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb index adf0c26bbc6..6f4728a1d98 100644 --- a/app/models/ml/candidate.rb +++ b/app/models/ml/candidate.rb @@ -29,7 +29,7 @@ module Ml scope: :project, init: AtomicInternalId.project_init(self, :internal_id) - scope :including_relationships, -> { includes(:latest_metrics, :params, :user, :package, :project) } + scope :including_relationships, -> { includes(:latest_metrics, :params, :user, :package, :project, :ci_build) } scope :by_name, ->(name) { where("ml_candidates.name LIKE ?", "%#{sanitize_sql_like(name)}%") } # rubocop:disable GitlabSecurity/SqlInjection scope :order_by_metric, ->(metric, direction) do @@ -69,6 +69,10 @@ module Ml iid end + def from_ci? + ci_build_id.present? + end + class << self def with_project_id_and_eid(project_id, eid) return unless project_id.present? && eid.present? diff --git a/app/models/service_desk/custom_email_verification.rb b/app/models/service_desk/custom_email_verification.rb index 69bd0f5e7f8..482a10447ed 100644 --- a/app/models/service_desk/custom_email_verification.rb +++ b/app/models/service_desk/custom_email_verification.rb @@ -77,6 +77,13 @@ module ServiceDesk verification.error = error verification.token = nil end + + # Supress warning: + # both enum and its state_machine have defined a different default for "state". + # State machine uses `nil` and the enum should use the same. + def owner_class_attribute_default + nil + end end # Needs to be below `state_machine` definition to suppress diff --git a/app/models/work_item.rb b/app/models/work_item.rb index 1d96dd9faff..11b83528a79 100644 --- a/app/models/work_item.rb +++ b/app/models/work_item.rb @@ -32,6 +32,14 @@ class WorkItem < Issue 'issues.id' end + # def reference_pattern + # # no-op: We currently only support link_reference_pattern parsing + # end + + def link_reference_pattern + @link_reference_pattern ||= compose_link_reference_pattern('work_items', Gitlab::Regex.work_item) + end + def work_item_children_keyset_order keyset_order = Gitlab::Pagination::Keyset::Order.build([ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml index af4c934fd72..40632e27fa7 100644 --- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml +++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml @@ -2,9 +2,10 @@ = render Pajamas::AlertComponent.new(title: s_('ClusterIntegration|Did you know?'), alert_options: { class: 'gcp-signup-offer', - data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }}) do |c| + data: { feature_id: Users::CalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: callouts_path }}, + close_button_options: { data: { track_action: 'click_dismiss', track_label: 'gcp_signup_offer_banner' }}) do |c| = c.body do = s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link } = c.actions do - = render Pajamas::ButtonComponent.new(variant: :confirm, href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', button_options: { rel: 'noopener noreferrer' }) do + = render Pajamas::ButtonComponent.new(variant: :confirm, href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', button_options: { rel: 'noopener noreferrer', data: { track_action: 'click_button', track_label: 'gcp_signup_offer_banner' } }) do = s_("ClusterIntegration|Apply for credit") diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml index e33e9509e3a..cfa0cf6d07b 100644 --- a/app/views/projects/branches/_commit.html.haml +++ b/app/views/projects/branches/_commit.html.haml @@ -6,4 +6,4 @@ %span.str-truncated = link_to_markdown commit.title, project_commit_path(project, commit.id), class: "commit-row-message cgray" · - #{time_ago_with_tooltip(commit.committed_date)} + %span.gl-text-secondary= time_ago_with_tooltip(commit.committed_date) diff --git a/app/views/projects/issues/_work_item_links.html.haml b/app/views/projects/issues/_work_item_links.html.haml index 911260308b4..981021c97e6 100644 --- a/app/views/projects/issues/_work_item_links.html.haml +++ b/app/views/projects/issues/_work_item_links.html.haml @@ -1,5 +1,5 @@ .js-work-item-links-root{ data: { issuable_id: @issue.id, - project_path: @project.full_path, + full_path: @project.full_path, wi: work_items_index_data(@project), register_path: new_user_registration_path(redirect_to_referer: 'yes'), sign_in_path: new_session_path(:user, redirect_to_referer: 'yes') } } diff --git a/app/views/projects/tags/_edit_release_button.html.haml b/app/views/projects/tags/_edit_release_button.html.haml index 1c2626e5612..9a6c18df2ca 100644 --- a/app/views/projects/tags/_edit_release_button.html.haml +++ b/app/views/projects/tags/_edit_release_button.html.haml @@ -5,5 +5,5 @@ - if release - release_btn_text = s_('TagsPage|Edit release') - release_btn_path = edit_project_release_path(project, release) -= link_to release_btn_path, class: css_classes, title: release_btn_text, data: { container: "body" } do - = sprite_icon('pencil', css_class: 'gl-icon') += link_to release_btn_path, class: css_classes do + = release_btn_text diff --git a/app/views/projects/tags/_release_link.html.haml b/app/views/projects/tags/_release_link.html.haml index 6c79b13f438..9284204af77 100644 --- a/app/views/projects/tags/_release_link.html.haml +++ b/app/views/projects/tags/_release_link.html.haml @@ -1,5 +1,5 @@ - if can?(current_user, :read_release, release) - .gl-text-secondary - = sprite_icon("rocket", size: 12) - = _("Release") + %span + = sprite_icon("rocket", size: 12, css_class: "gl-text-secondary") + = _("Release:") = link_to release.name, project_release_path(project, release), class: "gl-text-blue-600!" diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml index fcad8509a7d..cc49ff9e293 100644 --- a/app/views/projects/tags/_tag.html.haml +++ b/app/views/projects/tags/_tag.html.haml @@ -2,9 +2,9 @@ - release = @releases.find { |release| release.tag == tag.name } - commit_status = @tag_pipeline_statuses[tag.name] unless @tag_pipeline_statuses.nil? -%li.flex-row.js-tag-list{ class: "gl-white-space-normal! gl-align-items-flex-start!" } +%li.gl-justify-content-space-between{ class: "gl-md-display-flex! gl-align-items-flex-start!", data: { testid: 'tag-row' } } .row-main-content - = sprite_icon('tag') + = sprite_icon('tag', css_class: "gl-text-secondary") = link_to tag.name, project_tag_path(@project, tag.name), class: 'item-title ref-name' - if protected_tag?(@project, tag) @@ -21,12 +21,13 @@ = render 'release_link', project: @project, release: release - if tag.message.present? - %pre.wrap + %pre.wrap.gl-mt-3.gl-max-w-80 = strip_signature(tag.message) - .row-fixed-content.controls.flex-row + .row-fixed-content.flex-row - if tag.has_signature? - = render partial: 'projects/commit/signature', object: tag.signature + .gl-mr-3 + = render partial: 'projects/commit/signature', object: tag.signature - if commit_status = render 'ci/status/icon', size: 24, status: commit_status, option_css_classes: 'gl-display-inline-flex gl-vertical-align-middle gl-mr-5' @@ -34,8 +35,11 @@ .gl-display-inline-flex.gl-vertical-align-middle.gl-mr-5 %svg.s24 - = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name] - - if can?(current_user, :admin_tag, @project) = render 'edit_release_button', tag: tag, project: @project, release: release, option_css_classes: 'gl-mr-3!' + + .gl-mr-3 + = render 'projects/buttons/download', project: @project, ref: tag.name, pipeline: @tags_pipelines[tag.name] + + - if can?(current_user, :admin_tag, @project) = render 'projects/buttons/remove_tag', project: @project, tag: tag diff --git a/babel.config.js b/babel.config.js index 6838df3ad84..ddb8b568bed 100644 --- a/babel.config.js +++ b/babel.config.js @@ -5,6 +5,7 @@ let presets = [ '@babel/preset-env', { useBuiltIns: 'usage', + bugfixes: true, corejs: { version: coreJSVersion, proposals: true }, modules: false, }, diff --git a/config/events/20230508093658_gcp_signup_offer_click_click.yml b/config/events/20230508093658_gcp_signup_offer_click_click.yml new file mode 100644 index 00000000000..493a7733225 --- /dev/null +++ b/config/events/20230508093658_gcp_signup_offer_click_click.yml @@ -0,0 +1,21 @@ +--- +description: GCP offer banner Apply for credit is clicked +category: gcp_signup_offer_click +action: click +label_description: 'gcp_signup_offer_banner' +property_description: +value_description: +extra_properties: +identifiers: +product_section: ops +product_stage: deploy +product_group: group::environments +milestone: '16.0' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119940 +distributions: + - ce + - ee +tiers: + - free + - premium + - ultimate diff --git a/config/events/20230508093912_gcp_signup_offer_dismiss_dismiss.yml b/config/events/20230508093912_gcp_signup_offer_dismiss_dismiss.yml new file mode 100644 index 00000000000..f42cb7d8f2e --- /dev/null +++ b/config/events/20230508093912_gcp_signup_offer_dismiss_dismiss.yml @@ -0,0 +1,21 @@ +--- +description: GCP offer banner is dismissed +category: gcp_signup_offer_dismiss +action: dismiss +label_description: 'gcp_signup_offer_banner' +property_description: +value_description: +extra_properties: +identifiers: +product_section: ops +product_stage: deploy +product_group: group::environments +milestone: '16.0' +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119940 +distributions: + - ce + - ee +tiers: + - free + - premium + - ultimate diff --git a/config/feature_flags/development/disable_follow_users.yml b/config/feature_flags/development/disable_follow_users.yml index 9788ca520fd..ead3687f302 100644 --- a/config/feature_flags/development/disable_follow_users.yml +++ b/config/feature_flags/development/disable_follow_users.yml @@ -1,7 +1,7 @@ --- name: disable_follow_users introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/116023 -rollout_issue_url: +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/408886 milestone: '16.0' type: development group: group::authentication and authorization diff --git a/db/post_migrate/20230328030101_add_secureflag_training_provider.rb b/db/post_migrate/20230328030101_add_secureflag_training_provider.rb new file mode 100644 index 00000000000..4b32570ea56 --- /dev/null +++ b/db/post_migrate/20230328030101_add_secureflag_training_provider.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class AddSecureflagTrainingProvider < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + SECUREFLAG_DATA = { + name: 'SecureFlag', + description: "Get remediation advice with example code and recommended hands-on labs in a fully + interactive virtualised environment.", + url: "https://knowledge-base-api.secureflag.com/gitlab" + } + + class TrainingProvider < MigrationRecord + self.table_name = 'security_training_providers' + end + + def up + current_time = Time.current + timestamps = { created_at: current_time, updated_at: current_time } + + TrainingProvider.reset_column_information + TrainingProvider.upsert(SECUREFLAG_DATA.merge(timestamps)) + end + + def down + TrainingProvider.reset_column_information + TrainingProvider.find_by(name: SECUREFLAG_DATA[:name])&.destroy + end +end diff --git a/db/post_migrate/20230508093910_create_package_manager_name_index.rb b/db/post_migrate/20230508093910_create_package_manager_name_index.rb new file mode 100644 index 00000000000..e15f253a417 --- /dev/null +++ b/db/post_migrate/20230508093910_create_package_manager_name_index.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class CreatePackageManagerNameIndex < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'index_on_sbom_sources_package_manager_name' + + def up + return if index_exists_by_name?(:sbom_sources, INDEX_NAME) + + disable_statement_timeout do + execute <<~SQL + CREATE INDEX CONCURRENTLY #{INDEX_NAME} + ON sbom_sources + USING BTREE ((source->'package_manager'->>'name')) + SQL + end + end + + def down + remove_concurrent_index_by_name :sbom_sources, INDEX_NAME + end +end diff --git a/db/schema_migrations/20230328030101 b/db/schema_migrations/20230328030101 new file mode 100644 index 00000000000..0b50a16a514 --- /dev/null +++ b/db/schema_migrations/20230328030101 @@ -0,0 +1 @@ +eb05e37733efa95de5067d328a8e3dbe2fe696c95658bad5362893c04c8b89b6
\ No newline at end of file diff --git a/db/schema_migrations/20230508093910 b/db/schema_migrations/20230508093910 new file mode 100644 index 00000000000..d9b056e68f9 --- /dev/null +++ b/db/schema_migrations/20230508093910 @@ -0,0 +1 @@ +1e0b966332d5094050ea779ba6efefaa5c0c2a7d9f2ec05a1fa8a049bd6fcd84
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index c2e2ddd0e4d..cce1aac348a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -31644,6 +31644,8 @@ CREATE INDEX index_on_projects_path ON projects USING btree (path); CREATE INDEX index_on_routes_lower_path ON routes USING btree (lower((path)::text)); +CREATE INDEX index_on_sbom_sources_package_manager_name ON sbom_sources USING btree ((((source -> 'package_manager'::text) ->> 'name'::text))); + CREATE INDEX index_on_todos_user_project_target_and_state ON todos USING btree (user_id, project_id, target_type, target_id, id) WHERE ((state)::text = 'pending'::text); CREATE INDEX index_on_users_lower_email ON users USING btree (lower((email)::text)); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d37ee757c6b..d6187ea13ea 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -13184,7 +13184,7 @@ A software dependency used by a project. | <a id="dependencyid"></a>`id` | [`GlobalID!`](#globalid) | ID of the dependency. | | <a id="dependencylocation"></a>`location` | [`Location`](#location) | Information about where the dependency is located. | | <a id="dependencyname"></a>`name` | [`String!`](#string) | Name of the dependency. | -| <a id="dependencypackager"></a>`packager` | [`String`](#string) | Description of the tool used to manage the dependency. | +| <a id="dependencypackager"></a>`packager` | [`PackageManager`](#packagemanager) | Description of the tool used to manage the dependency. | | <a id="dependencyversion"></a>`version` | [`String`](#string) | Version of the dependency. | ### `DependencyProxyBlob` @@ -15820,6 +15820,24 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). | <a id="groupvaluestreamanalyticsflowmetricsissuecountprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. | | <a id="groupvaluestreamanalyticsflowmetricsissuecountto"></a>`to` | [`Time!`](#time) | Before the date. | +##### `GroupValueStreamAnalyticsFlowMetrics.issuesCompletedCount` + +Number of open issues closed (completed) in the given period. Maximum value is 10,001. + +Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | +| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | +| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountfrom"></a>`from` | [`Time!`](#time) | After the date. | +| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountlabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. | +| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. | +| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountprojectids"></a>`projectIds` | [`[ID!]`](#id) | Project IDs within the group hierarchy. | +| <a id="groupvaluestreamanalyticsflowmetricsissuescompletedcountto"></a>`to` | [`Time!`](#time) | Before the date. | + ##### `GroupValueStreamAnalyticsFlowMetrics.leadTime` Median time from when the issue was created to when it was closed. @@ -19229,6 +19247,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="projectdependenciespackagemanagers"></a>`packageManagers` | [`[PackageManager!]`](#packagemanager) | Filter dependencies by package managers. | | <a id="projectdependenciessort"></a>`sort` | [`DependencySort`](#dependencysort) | Sort dependencies by given criteria. | ##### `Project.deployment` @@ -20460,6 +20479,23 @@ Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). | <a id="projectvaluestreamanalyticsflowmetricsissuecountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. | | <a id="projectvaluestreamanalyticsflowmetricsissuecountto"></a>`to` | [`Time!`](#time) | Before the date. | +##### `ProjectValueStreamAnalyticsFlowMetrics.issuesCompletedCount` + +Number of open issues closed (completed) in the given period. Maximum value is 10,001. + +Returns [`ValueStreamAnalyticsMetric`](#valuestreamanalyticsmetric). + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountassigneeusernames"></a>`assigneeUsernames` | [`[String!]`](#string) | Usernames of users assigned to the issue. | +| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountauthorusername"></a>`authorUsername` | [`String`](#string) | Username of the author of the issue. | +| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountfrom"></a>`from` | [`Time!`](#time) | After the date. | +| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountlabelnames"></a>`labelNames` | [`[String!]`](#string) | Labels applied to the issue. | +| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountmilestonetitle"></a>`milestoneTitle` | [`String`](#string) | Milestone applied to the issue. | +| <a id="projectvaluestreamanalyticsflowmetricsissuescompletedcountto"></a>`to` | [`Time!`](#time) | Before the date. | + ##### `ProjectValueStreamAnalyticsFlowMetrics.leadTime` Median time from when the issue was created to when it was closed. @@ -24747,6 +24783,27 @@ Values for sorting group packages. | <a id="packagegroupsortversion_asc"></a>`VERSION_ASC` | Ordered by version in ascending order. | | <a id="packagegroupsortversion_desc"></a>`VERSION_DESC` | Ordered by version in descending order. | +### `PackageManager` + +Values for package manager. + +| Value | Description | +| ----- | ----------- | +| <a id="packagemanagerbundler"></a>`BUNDLER` | Package manager: bundler. | +| <a id="packagemanagercomposer"></a>`COMPOSER` | Package manager: composer. | +| <a id="packagemanagerconan"></a>`CONAN` | Package manager: conan. | +| <a id="packagemanagergo"></a>`GO` | Package manager: go. | +| <a id="packagemanagergradle"></a>`GRADLE` | Package manager: gradle. | +| <a id="packagemanagermaven"></a>`MAVEN` | Package manager: maven. | +| <a id="packagemanagernpm"></a>`NPM` | Package manager: npm. | +| <a id="packagemanagernuget"></a>`NUGET` | Package manager: nuget. | +| <a id="packagemanagerpip"></a>`PIP` | Package manager: pip. | +| <a id="packagemanagerpipenv"></a>`PIPENV` | Package manager: pipenv. | +| <a id="packagemanagerpnpm"></a>`PNPM` | Package manager: pnpm. | +| <a id="packagemanagersbt"></a>`SBT` | Package manager: sbt. | +| <a id="packagemanagersetuptools"></a>`SETUPTOOLS` | Package manager: setuptools. | +| <a id="packagemanageryarn"></a>`YARN` | Package manager: yarn. | + ### `PackageSort` Values for sorting package. diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index c9f31b36e3f..18d962451e4 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -151,6 +151,20 @@ change, you must update the `CODEOWNERS` file: 1. Add and commit all your changes and push your branch up to `origin`. 1. Create a merge request and assign it to a technical writing manager for review. +When updating the `codeowners.rake` file: + +- To specify multiple writers for a single group, use a space between writer names: + + ```plaintext + CodeOwnerRule.new('Group Name', '@writer1 @writer2'), + ``` + +- For a group that does not have an assigned writer, include the group name in the file and comment out the line: + + ```plaintext + # CodeOwnerRule.new('Group Name', ''), + ``` + ## Move, rename, or delete a page See [redirects](redirects.md). diff --git a/doc/development/service_ping/metrics_lifecycle.md b/doc/development/service_ping/metrics_lifecycle.md index 3c51eefc4b4..318db6895fb 100644 --- a/doc/development/service_ping/metrics_lifecycle.md +++ b/doc/development/service_ping/metrics_lifecycle.md @@ -14,57 +14,21 @@ Follow the [Implement Service Ping](implement.md) guide. ## Change an existing metric -See [this video tutorial](https://youtu.be/bYf3c01KCls) for help with the update of metric attributes. - -NOTE: -The `key_path` attribute represents the location of the metric in Service Ping payload and must not be changed. - -Because we do not control when customers update their self-managed instances of GitLab, -we **STRONGLY DISCOURAGE** changes to the logic used to calculate any metric. -Any such changes lead to inconsistent reports from multiple GitLab instances. -If there is a problem with an existing metric, it's best to deprecate the existing metric, -and use it, side by side, with the desired new metric. - -If you do need to change a metric, please notify the Customer Success Ops team (`@csops-team`), Analytics Engineers (`@gitlab-data/analytics-engineers`), and Product Analysts (`@gitlab-data/product-analysts`) teams by `@` mentioning those groups in a comment on the MR. -Many Service Ping metrics are relied upon for health score and XMAU reporting and -unexpected changes to those metrics could break reporting. - -Example: -Consider following change. Before GitLab 12.6, the `example_metric` was implemented as: - -```ruby -{ - ... - example_metric: distinct_count(Project, :creator_id) -} -``` - -For GitLab 12.6, the metric was changed to filter out archived projects: - -```ruby -{ - ... - example_metric: distinct_count(Project.non_archived, :creator_id) -} -``` +WARNING: +We want to **PREVENT** changes to the calculation logic or important attributes on any metric as this invalidates comparisons of the same metric across different versions of GitLab. -In this scenario, all instances running up to GitLab 12.5 continue to report `example_metric`, -including all archived projects, while all instances running GitLab 12.6 and higher filters -out such projects. As Service Ping data is collected from all reporting instances, the -resulting dataset includes mixed data, which distorts any following business analysis. +If you change a metric, you have to consider that not all instances of GitLab are running on the newest version. Old instances will still report the old version of the metric. +Additionally, a metric's reported numbers are primarily interesting compared to previously reported numbers. +As a result, if you need to change one of the following parts of a metric, you need to add a new metric instead. It's your choice whether to keep the old metric alongside the new one or [remove it](#remove-a-metric). -The correct approach is to add a new metric for GitLab 12.6 release with updated logic: +- **calculation logic**: This means any changes that can produce a different value than the previous implementation +- **YAML attributes**: The following attributes are directly used for analysis or calculation: `key_path`, `time_frame`, `value_type`, `data_source`. -```ruby -{ - ... - example_metric_without_archived: distinct_count(Project.non_archived, :creator_id) -} -``` +If you change the `performance_indicator_type` attribute of a metric or think your case needs an exception from the outlined rules then please notify the Customer Success Ops team (`@csops-team`), Analytics Engineers (`@gitlab-data/analytics-engineers`), and Product Analysts (`@gitlab-data/product-analysts`) teams by `@` mentioning those groups in a comment on the merge request or issue. -and update existing business analysis artefacts to use `example_metric_without_archived` instead of `example_metric` +You can change any other attributes without impact to the calculation or analysis. See [this video tutorial](https://youtu.be/bYf3c01KCls) for help updating metric attributes. -Currently, the [Metrics Dictionary](https://metrics.gitlab.com/) is built automatically once a day. When a change to a metric is made in a YAML file, you can see the change in the dictionary within 24 hours. +Currently, the [Metrics Dictionary](https://metrics.gitlab.com/) is built automatically once a day. You can see the change in the dictionary within 24 hours when you change the metric's YAML file. ## Remove a metric diff --git a/doc/user/markdown.md b/doc/user/markdown.md index ec1ed05af93..104c633229a 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -669,19 +669,21 @@ In addition to this, links to some objects are also recognized and formatted. So ### Show the issue, merge request, or epic title in the reference -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15694) in GitLab 14.6. +> - Support for issues, merge requests, and epics [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15694) in GitLab 14.6. +> - Support for work items [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390854) in GitLab 16.0. -To include the title in the rendered link of an issue, merge request, or epic, add a plus (`+`) +To include the title in the rendered link of an issue, work item, merge request, or epic, add a plus (`+`) at the end of the reference. For example, a reference like `#123+` is rendered as `The issue title (#123)`. URL references like `https://gitlab.com/gitlab-org/gitlab/-/issues/1234+` are also expanded. -### Show the issue or merge request summary in the reference +### Show the issue, work item or merge request summary in the reference -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/386937) in GitLab 15.10. +> - Support for issues and merge requests [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/386937) in GitLab 15.10. +> - Support for work items [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/390854) in GitLab 16.0. -To include an extended summary in the rendered link of an issue or merge request, add a `+s` +To include an extended summary in the rendered link of an issue, work item, or merge request, add a `+s` at the end of the reference. Summary includes information about **assignees**, **milestone** and **health status** of referenced item. diff --git a/lib/banzai/filter/issuable_reference_expansion_filter.rb b/lib/banzai/filter/issuable_reference_expansion_filter.rb index 8fe1c90b314..ec7778a3630 100644 --- a/lib/banzai/filter/issuable_reference_expansion_filter.rb +++ b/lib/banzai/filter/issuable_reference_expansion_filter.rb @@ -90,7 +90,7 @@ module Banzai end def moved_issue?(issuable) - issuable.instance_of?(Issue) && issuable.moved? + issuable.is_a?(Issue) && issuable.moved? end def should_expand?(node, issuable) diff --git a/lib/banzai/filter/references/work_item_reference_filter.rb b/lib/banzai/filter/references/work_item_reference_filter.rb new file mode 100644 index 00000000000..ed62b9a1be1 --- /dev/null +++ b/lib/banzai/filter/references/work_item_reference_filter.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Banzai + module Filter + module References + # HTML filter that replaces work item references with links. References to + # work items that do not exist are ignored. + # + # This filter supports cross-project references. + class WorkItemReferenceFilter < IssueReferenceFilter + self.reference_type = :work_item + self.object_class = WorkItem + + def parent_records(parent, ids) + parent.work_items.where(iid: ids.to_a) + end + + private + + def additional_object_attributes(work_item) + { work_item_type: work_item.work_item_type.base_type } + end + end + end + end +end diff --git a/lib/banzai/issuable_extractor.rb b/lib/banzai/issuable_extractor.rb index 34b6ca99e32..6428f71eb8f 100644 --- a/lib/banzai/issuable_extractor.rb +++ b/lib/banzai/issuable_extractor.rb @@ -12,6 +12,7 @@ module Banzai attr_reader :context ISSUE_REFERENCE_TYPE = '@data-reference-type="issue"' + WORK_ITEM_REFERENCE_TYPE = '@data-reference-type="work_item"' MERGE_REQUEST_REFERENCE_TYPE = '@data-reference-type="merge_request"' # context - An instance of Banzai::RenderContext. @@ -41,6 +42,7 @@ module Banzai def parsers [ Banzai::ReferenceParser::IssueParser.new(context), + Banzai::ReferenceParser::WorkItemParser.new(context), Banzai::ReferenceParser::MergeRequestParser.new(context) ] end @@ -53,7 +55,7 @@ module Banzai end def reference_types - [ISSUE_REFERENCE_TYPE, MERGE_REQUEST_REFERENCE_TYPE] + [ISSUE_REFERENCE_TYPE, WORK_ITEM_REFERENCE_TYPE, MERGE_REQUEST_REFERENCE_TYPE] end end end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index 1fd565999f5..53f938c044f 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -58,6 +58,7 @@ module Banzai Filter::References::ProjectReferenceFilter, Filter::References::DesignReferenceFilter, Filter::References::IssueReferenceFilter, + Filter::References::WorkItemReferenceFilter, Filter::References::ExternalIssueReferenceFilter, Filter::References::MergeRequestReferenceFilter, Filter::References::SnippetReferenceFilter, diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb index 1833d8239d6..d0e74044bba 100644 --- a/lib/banzai/reference_parser/issue_parser.rb +++ b/lib/banzai/reference_parser/issue_parser.rb @@ -57,7 +57,17 @@ module Banzai end def records_for_nodes(nodes) - node_includes = [ + @issues_for_nodes ||= grouped_objects_for_nodes( + nodes, + Issue.all.includes(node_includes), + self.class.data_attribute + ) + end + + private + + def node_includes + includes = [ :work_item_type, :namespace, :author, @@ -69,13 +79,9 @@ module Banzai project: [:namespace, :project_feature, :route] } ] - node_includes << :milestone if context.options[:extended_preload] + includes << :milestone if context.options[:extended_preload] - @issues_for_nodes ||= grouped_objects_for_nodes( - nodes, - Issue.all.includes(node_includes), - self.class.data_attribute - ) + includes end end end diff --git a/lib/banzai/reference_parser/work_item_parser.rb b/lib/banzai/reference_parser/work_item_parser.rb new file mode 100644 index 00000000000..1ce0b067687 --- /dev/null +++ b/lib/banzai/reference_parser/work_item_parser.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Banzai + module ReferenceParser + class WorkItemParser < IssueParser + self.reference_type = :work_item + + def records_for_nodes(nodes) + @work_items_for_nodes ||= grouped_objects_for_nodes( + nodes, + WorkItem.all.includes(node_includes), + self.class.data_attribute + ) + end + end + end +end diff --git a/lib/gitlab/ci/config/external/file/project.rb b/lib/gitlab/ci/config/external/file/project.rb index 16c5375b8bb..16a6bc8a692 100644 --- a/lib/gitlab/ci/config/external/file/project.rb +++ b/lib/gitlab/ci/config/external/file/project.rb @@ -89,7 +89,9 @@ module Gitlab return if project.nil? - # with `itself`, we are force-loading the project + # We are force-loading the project with the `itself` method + # because the `project` variable can be a `BatchLoader` object and we should not + # pass a `BatchLoader` object in the `for` method to prevent unwanted behaviors. BatchLoader.for(project.itself) .batch(key: context.user) do |projects, loader, args| projects.uniq.each do |project| diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index 0f9e7daf4b8..63242d60c85 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -104,8 +104,14 @@ module Gitlab end def validate_duplicate_needs!(name, needs) - unless needs.uniq == needs - error!("#{name} has duplicate entries in the needs section.") + duplicated_needs = + needs + .group_by { |need| need[:name] } + .select { |_, items| items.count > 1 } + .keys + + unless duplicated_needs.empty? + error!("#{name} has the following needs duplicated: #{duplicated_needs.join(', ')}.") end end diff --git a/lib/gitlab/database_importers/security/training_providers/importer.rb b/lib/gitlab/database_importers/security/training_providers/importer.rb index aa6a9f29c6d..87bef6400fa 100644 --- a/lib/gitlab/database_importers/security/training_providers/importer.rb +++ b/lib/gitlab/database_importers/security/training_providers/importer.rb @@ -20,6 +20,13 @@ module Gitlab url: "https://integration-api.securecodewarrior.com/api/v1/trial" }.freeze + SECUREFLAG_DATA = { + name: 'SecureFlag', + description: "Get remediation advice with example code and recommended hands-on labs in a fully + interactive virtualised environment.", + url: "https://knowledge-base-api.secureflag.com/gitlab" + }.freeze + module Security class TrainingProvider < ApplicationRecord self.table_name = 'security_training_providers' @@ -31,7 +38,7 @@ module Gitlab timestamps = { created_at: current_time, updated_at: current_time } Security::TrainingProvider.upsert_all( - [KONTRA_DATA.merge(timestamps), SCW_DATA.merge(timestamps)], + [KONTRA_DATA.merge(timestamps), SCW_DATA.merge(timestamps), SECUREFLAG_DATA.merge(timestamps)], unique_by: :index_security_training_providers_on_unique_name ) end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index af11bcf06b0..ef8cb38029a 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -68,6 +68,8 @@ module Gitlab push_frontend_feature_flag(:vscode_web_ide, current_user) push_frontend_feature_flag(:super_sidebar_peek, current_user) push_frontend_feature_flag(:unbatch_graphql_queries) + # To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248 + push_frontend_feature_flag(:remove_monitor_metrics) end # Exposes the state of a feature flag to the frontend code. diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index 3640edbaa26..eb99805e2e8 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -594,6 +594,10 @@ module Gitlab @issue ||= /(?<issue>\d+)(?<format>\+s{,1})?(?=\W|\z)/ end + def work_item + @work_item ||= /(?<work_item>\d+)(?<format>\+s{,1})?(?=\W|\z)/ + end + def merge_request @merge_request ||= /(?<merge_request>\d+)(?<format>\+s{,1})?/ end diff --git a/lib/gitlab/saas.rb b/lib/gitlab/saas.rb index 16a7a697e6a..722475ce61d 100644 --- a/lib/gitlab/saas.rb +++ b/lib/gitlab/saas.rb @@ -49,6 +49,10 @@ module Gitlab "https://about.gitlab.com/pricing#faq" end + def self.about_feature_comparison_url + "https://about.gitlab.com/pricing/gitlab-com/feature-comparison" + end + def self.doc_url 'https://docs.gitlab.com' end diff --git a/lib/tasks/gitlab/tw/codeowners.rake b/lib/tasks/gitlab/tw/codeowners.rake index 4d43dd2dd85..b4b34581f43 100644 --- a/lib/tasks/gitlab/tw/codeowners.rake +++ b/lib/tasks/gitlab/tw/codeowners.rake @@ -39,7 +39,7 @@ namespace :tw do CodeOwnerRule.new('Development', '@sselhorn'), CodeOwnerRule.new('Distribution', '@axil'), CodeOwnerRule.new('Distribution (Charts)', '@axil'), - CodeOwnerRule.new('Distribution (Omnibus)', '@axil'), + CodeOwnerRule.new('Distribution (Omnibus)', '@eread'), CodeOwnerRule.new('Documentation Guidelines', '@sselhorn'), CodeOwnerRule.new('Dynamic Analysis', '@rdickenson'), CodeOwnerRule.new('IDE', '@ashrafkhamis'), @@ -50,9 +50,8 @@ namespace :tw do CodeOwnerRule.new('Gitaly', '@eread'), CodeOwnerRule.new('GitLab Dedicated', '@drcatherinepope'), CodeOwnerRule.new('Global Search', '@ashrafkhamis'), - CodeOwnerRule.new('Import', '@eread'), + CodeOwnerRule.new('Import and Integrate', '@eread @ashrafkhamis'), CodeOwnerRule.new('Infrastructure', '@sselhorn'), - CodeOwnerRule.new('Integrations', '@ashrafkhamis'), # CodeOwnerRule.new('Knowledge', ''), # CodeOwnerRule.new('MLOps', '') CodeOwnerRule.new('Observability', '@drcatherinepope'), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 118c9e08558..70764610ff7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1319,6 +1319,12 @@ msgstr "" msgid "'%{value}' days of inactivity must be greater than or equal to 90" msgstr "" +msgid "'projects' is not yet supported" +msgstr "" + +msgid "'starterProjects' is not yet supported" +msgstr "" + msgid "(%d closed)" msgid_plural "(%d closed)" msgstr[0] "" @@ -5073,6 +5079,9 @@ msgstr "" msgid "Analytics|Browser Family" msgstr "" +msgid "Analytics|Cancel" +msgstr "" + msgid "Analytics|Choose a chart type on the right" msgstr "" @@ -5088,6 +5097,9 @@ msgstr "" msgid "Analytics|Configure Dashboard Project" msgstr "" +msgid "Analytics|Create dashboard %{dashboardId}" +msgstr "" + msgid "Analytics|Custom dashboards" msgstr "" @@ -5112,7 +5124,7 @@ msgstr "" msgid "Analytics|Edit" msgstr "" -msgid "Analytics|Error while saving Dashboard!" +msgid "Analytics|Error while saving dashboard" msgstr "" msgid "Analytics|Host" @@ -5127,6 +5139,9 @@ msgstr "" msgid "Analytics|New Analytics Visualization Title" msgstr "" +msgid "Analytics|New dashboard" +msgstr "" + msgid "Analytics|No dashboard matches the specified URL path." msgstr "" @@ -9247,6 +9262,9 @@ msgstr "" msgid "Checkout|Payment method" msgstr "" +msgid "Checkout|Pricing reflective of %{linkStart}limited-time offer%{linkEnd}." +msgstr "" + msgid "Checkout|Purchase details" msgstr "" @@ -23674,7 +23692,7 @@ msgstr "" msgid "Inherited:" msgstr "" -msgid "Inheriting from parent is not yet supported" +msgid "Inheriting from 'parent' is not yet supported" msgstr "" msgid "Initial default branch name" @@ -24770,6 +24788,9 @@ msgstr "" msgid "Issues" msgstr "" +msgid "Issues Completed" +msgstr "" + msgid "Issues Rate Limits" msgstr "" @@ -28753,6 +28774,9 @@ msgstr "" msgid "MlExperimentTracking|Author" msgstr "" +msgid "MlExperimentTracking|CI Job" +msgstr "" + msgid "MlExperimentTracking|Candidate removed" msgstr "" @@ -34009,9 +34033,6 @@ msgstr "" msgid "ProductAnalytics|Back to dashboards" msgstr "" -msgid "ProductAnalytics|Cancel Edit" -msgstr "" - msgid "ProductAnalytics|Click Events" msgstr "" @@ -37167,6 +37188,9 @@ msgstr "" msgid "Release with tag \"%{tag}\" was not found" msgstr "" +msgid "Release:" +msgstr "" + msgid "ReleaseAssetLinkType|Image" msgstr "" @@ -53300,7 +53324,9 @@ msgid "is too long (maximum is 1000 entries)" msgstr "" msgid "issue" -msgstr "" +msgid_plural "issues" +msgstr[0] "" +msgstr[1] "" msgid "issues at risk" msgstr "" diff --git a/rubocop/cop/rspec/avoid_conditional_statements.rb b/rubocop/cop/rspec/avoid_conditional_statements.rb new file mode 100644 index 00000000000..48c230a6a7a --- /dev/null +++ b/rubocop/cop/rspec/avoid_conditional_statements.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rubocop-rspec' + +module RuboCop + module Cop + module RSpec + # This cop checks for the usage of conditional statements in specs. + # + # @example + # + # # bad + # + # page.has_css?('[data-testid="begin-commit-button"]') ? find('[data-testid="begin-commit-button"]').click : nil + # + # if page.has_css?('[data-testid="begin-commit-button"]') + # find('[data-testid="begin-commit-button"]').click + # end + # + # unless page.has_css?('[data-testid="begin-commit-button"]') + # find('[data-testid="begin-commit-button"]').click + # end + class AvoidConditionalStatements < RuboCop::Cop::Base + MESSAGE = "Don't use `%{conditional}` conditional statments in specs, it might create flakiness. " \ + "See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109" + + def on_if(node) + conditional = node.ternary? ? "#{node.condition.to_s.delete!("\n")} ? (ternary)" : node.keyword + + add_offense(node, message: format(MESSAGE, conditional: conditional)) + end + end + end + end +end diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb index adf0c907adb..10764457d84 100644 --- a/spec/factories/work_items.rb +++ b/spec/factories/work_items.rb @@ -14,6 +14,19 @@ FactoryBot.define do confidential { true } end + trait :opened do + state_id { WorkItem.available_states[:opened] } + end + + trait :locked do + discussion_locked { true } + end + + trait :closed do + state_id { WorkItem.available_states[:closed] } + closed_at { Time.now } + end + trait :task do issue_type { :task } association :work_item_type, :default, :task diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index 8aecaab42c2..b6196fa6a1d 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -138,7 +138,7 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do wait_for_requests end - it 'moves to end of list' do + it 'moves to end of list', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410100' do expect(all('.board-card').first).to have_content(issue3.title) page.within(find('.board:nth-child(2)')) do @@ -151,7 +151,7 @@ RSpec.describe 'Issue Boards', :js, feature_category: :team_planning do expect(all('.board-card').last).to have_content(issue3.title) end - it 'moves to start of list' do + it 'moves to start of list', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410100' do expect(all('.board-card').last).to have_content(issue1.title) page.within(find('.board:nth-child(2)')) do diff --git a/spec/features/markdown/markdown_spec.rb b/spec/features/markdown/markdown_spec.rb index 7a4c7529711..a31ad5a868e 100644 --- a/spec/features/markdown/markdown_spec.rb +++ b/spec/features/markdown/markdown_spec.rb @@ -250,6 +250,7 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures, feature_category: :team_p aggregate_failures 'all reference filters' do expect(doc).to reference_users expect(doc).to reference_issues + expect(doc).to reference_work_items expect(doc).to reference_merge_requests expect(doc).to reference_snippets expect(doc).to reference_commit_ranges @@ -345,6 +346,7 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures, feature_category: :team_p aggregate_failures 'all reference filters' do expect(doc).to reference_users expect(doc).to reference_issues + expect(doc).to reference_work_items expect(doc).to reference_merge_requests expect(doc).to reference_snippets expect(doc).to reference_commit_ranges diff --git a/spec/features/markdown/metrics_spec.rb b/spec/features/markdown/metrics_spec.rb index 45b5d2f78e8..9f00bb99c0d 100644 --- a/spec/features/markdown/metrics_spec.rb +++ b/spec/features/markdown/metrics_spec.rb @@ -17,6 +17,7 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st let(:metrics_url) { urls.metrics_project_environment_url(project, environment) } before do + stub_feature_flags(remove_monitor_metrics: false) clear_host_from_memoized_variables stub_gitlab_domain @@ -50,6 +51,20 @@ RSpec.describe 'Metrics rendering', :js, :kubeclient, :use_clean_rails_memory_st .at_least(:once) end + context 'with remove_monitor_metrics flag enabled' do + before do + stub_feature_flags(remove_monitor_metrics: true) + end + + it 'does not show embedded metrics' do + visit project_issue_path(project, issue) + + expect(page).not_to have_css('div.prometheus-graph') + expect(page).not_to have_text('Memory Usage (Total)') + expect(page).not_to have_text('Core Usage (Total)') + end + end + context 'when dashboard params are in included the url' do let(:metrics_url) { urls.metrics_project_environment_url(project, environment, **chart_params) } diff --git a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb index 06276d2a933..e5352ad88ce 100644 --- a/spec/features/merge_request/user_sees_discussions_navigation_spec.rb +++ b/spec/features/merge_request/user_sees_discussions_navigation_spec.rb @@ -42,7 +42,7 @@ RSpec.describe 'Merge request > User sees discussions navigation', :js, feature_ shared_examples 'a page with a thread navigation' do context 'with active threads' do - it 'navigates to the first thread' do + it 'navigates to the first thread', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410144' do goto_next_thread expect(page).to have_selector(first_discussion_selector, obscured: false) end diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index 3bb4e93dc64..038d58a20ab 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -370,7 +370,7 @@ RSpec.describe 'Environments page', :js, feature_category: :projects do sha: project.commit.id) end - it 'does not show deployments' do + it 'does not show deployments', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/409990' do visit_environments(project) page.click_button _('Expand') diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index c0e41ba88aa..06d894fd5f1 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -302,7 +302,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do click_button('manual build') end - it 'enqueues manual action job' do + it 'enqueues manual action job', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/409984' do expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] .gl-dropdown-toggle:disabled') end end @@ -369,7 +369,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :projects do wait_for_requests end - it 'enqueues the delayed job', :js do + it 'enqueues the delayed job', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/410129' do expect(delayed_job.reload).to be_pending end end diff --git a/spec/features/tags/developer_deletes_tag_spec.rb b/spec/features/tags/developer_deletes_tag_spec.rb index 76cf3aa691d..19feb5b21bc 100644 --- a/spec/features/tags/developer_deletes_tag_spec.rb +++ b/spec/features/tags/developer_deletes_tag_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_management do + include Spec::Support::Helpers::ModalHelpers + let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, :repository, namespace: group) } @@ -18,7 +20,7 @@ RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_mana it 'deletes the tag' do expect(page).to have_content 'v1.1.0' - container = page.find('.content .flex-row', text: 'v1.1.0') + container = page.find('[data-testid="tag-row"]', text: 'v1.1.0') delete_tag container expect(page).not_to have_content 'v1.1.0' @@ -28,7 +30,7 @@ RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_mana it 'can not delete protected tags' do expect(page).to have_content 'v1.1.1' - container = page.find('.content .flex-row', text: 'v1.1.1') + container = page.find('[data-testid="tag-row"]', text: 'v1.1.1') expect(container).to have_button('Only a project maintainer or owner can delete a protected tag', disabled: true) end @@ -41,8 +43,7 @@ RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_mana expect(page).to have_current_path( project_tag_path(project, 'v1.0.0'), ignore_query: true) - container = page.find('.nav-controls') - delete_tag container + delete_tag expect(page).to have_current_path(project_tags_path(project), ignore_query: true) expect(page).not_to have_content 'v1.0.0' @@ -58,17 +59,22 @@ RSpec.describe 'Developer deletes tag', :js, feature_category: :source_code_mana end it 'shows the error message' do - container = page.find('.content .flex-row', text: 'v1.1.0') + container = page.find('[data-testid="tag-row"]', text: 'v1.1.0') delete_tag container expect(page).to have_content('Do not delete tags') end end - def delete_tag(container) - container.find('.js-delete-tag-button').click + def delete_tag(container = page.document) + within container do + click_button('Delete tag') + end + + within_modal do + click_button('Yes, delete tag') + end - page.within('.modal') { click_button('Yes, delete tag') } wait_for_requests end end diff --git a/spec/features/tags/maintainer_deletes_protected_tag_spec.rb b/spec/features/tags/maintainer_deletes_protected_tag_spec.rb index ce518b962cd..67f6862502c 100644 --- a/spec/features/tags/maintainer_deletes_protected_tag_spec.rb +++ b/spec/features/tags/maintainer_deletes_protected_tag_spec.rb @@ -19,7 +19,10 @@ RSpec.describe 'Maintainer deletes protected tag', :js, feature_category: :sourc it 'deletes the tag' do expect(page).to have_content "#{tag_name} protected" - page.find('.content .flex-row', text: tag_name).find('.js-delete-tag-button').click + page.within('[data-testid="tag-row"]', text: tag_name) do + click_button('Delete tag') + end + assert_modal_content(tag_name) confirm_delete_tag(tag_name) @@ -35,7 +38,7 @@ RSpec.describe 'Maintainer deletes protected tag', :js, feature_category: :sourc it 'deletes the tag' do expect(page).to have_current_path(project_tag_path(project, tag_name), ignore_query: true) - page.find('.js-delete-tag-button').click + click_button('Delete tag') assert_modal_content(tag_name) confirm_delete_tag(tag_name) diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 26e5f110687..fa73cd53a66 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -194,6 +194,19 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Link to issue by reference: [Issue](<%= issue.to_reference %>) - Link to issue by URL: [Issue](<%= urls.project_issue_url(issue.project, issue) %>) +#### WorkItemReferenceFilter + +Note: work item references use `#`, which get built as an issue link. + +- Work item (counted as an issue reference): <%= work_item.to_reference %> +- Work item in another project (counted as an issue reference): <%= xwork_item.to_reference(project) %> +- Ignored in code: `<%= work_item.to_reference %>` +- Ignored in links: [Link to <%= work_item.to_reference %>](#work_item-link) +- Ignored when backslash escaped: \<%= work_item.to_reference %> +- Work item by URL: <%= urls.project_work_item_url(work_item.project, work_item) %> +- Link to work item by reference (counted as an issue reference): [Work item](<%= work_item.to_reference %>) +- Link to work item by URL: [Work item](<%= urls.project_work_item_url(work_item.project, work_item) %>) + #### MergeRequestReferenceFilter - Merge request: <%= merge_request.to_reference %> diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js index 0bbb92282e5..220ad874b47 100644 --- a/spec/frontend/behaviors/markdown/render_gfm_spec.js +++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js @@ -1,4 +1,7 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm'; +import renderMetrics from '~/behaviors/markdown/render_metrics'; + +jest.mock('~/behaviors/markdown/render_metrics'); describe('renderGFM', () => { it('handles a missing element', () => { @@ -6,4 +9,27 @@ describe('renderGFM', () => { renderGFM(); }).not.toThrow(); }); + + describe('remove_monitor_metrics flag', () => { + let metricsElement; + + beforeEach(() => { + window.gon = { features: { removeMonitorMetrics: true } }; + metricsElement = document.createElement('div'); + metricsElement.setAttribute('class', '.js-render-metrics'); + }); + + it('renders metrics when the flag is disabled', () => { + window.gon.features = { features: { removeMonitorMetrics: false } }; + renderGFM(metricsElement); + + expect(renderMetrics).toHaveBeenCalled(); + }); + + it('does not render metrics when the flag is enabled', () => { + renderGFM(metricsElement); + + expect(renderMetrics).not.toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js index e4ca84853c3..f4e93e83ce8 100644 --- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js +++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js @@ -61,44 +61,52 @@ describe('RunnerListEmptyState', () => { expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`); }); - describe('when create_runner_workflow_for_admin is enabled', () => { - beforeEach(() => { - createComponent({ - provide: { - glFeatures: { createRunnerWorkflowForAdmin: true }, - }, + describe.each([ + { createRunnerWorkflowForAdmin: true }, + { createRunnerWorkflowForNamespace: true }, + ])('when %o', (glFeatures) => { + describe('when newRunnerPath is defined', () => { + beforeEach(() => { + createComponent({ + provide: { + glFeatures, + }, + }); }); - }); - it('shows a link to the new runner page', () => { - expect(findLink().attributes('href')).toBe(newRunnerPath); + it('shows a link to the new runner page', () => { + expect(findLink().attributes('href')).toBe(newRunnerPath); + }); }); - }); - describe('when create_runner_workflow_for_admin is enabled and newRunnerPath not defined', () => { - beforeEach(() => { - createComponent({ - props: { - newRunnerPath: null, - }, - provide: { - glFeatures: { createRunnerWorkflowForAdmin: true }, - }, + describe('when newRunnerPath not defined', () => { + beforeEach(() => { + createComponent({ + props: { + newRunnerPath: null, + }, + provide: { + glFeatures, + }, + }); }); - }); - it('opens a runner registration instructions modal with a link', () => { - const { value } = getBinding(findLink().element, 'gl-modal'); + it('opens a runner registration instructions modal with a link', () => { + const { value } = getBinding(findLink().element, 'gl-modal'); - expect(findRunnerInstructionsModal().props('modalId')).toEqual(value); + expect(findRunnerInstructionsModal().props('modalId')).toEqual(value); + }); }); }); - describe('when create_runner_workflow_for_admin is disabled', () => { + describe.each([ + { createRunnerWorkflowForAdmin: false }, + { createRunnerWorkflowForNamespace: false }, + ])('when %o', (glFeatures) => { beforeEach(() => { createComponent({ provide: { - glFeatures: { createRunnerWorkflowForAdmin: false }, + glFeatures, }, }); }); diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js index 5b2ddeafe04..e750327294c 100644 --- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js @@ -420,7 +420,13 @@ describe('GroupRunnersApp', () => { }); it('shows an empty state', () => { - expect(findRunnerListEmptyState().exists()).toBe(true); + expect(findRunnerListEmptyState().props()).toMatchObject({ + isSearchFiltered: false, + newRunnerPath, + registrationToken: mockRegistrationToken, + svgPath: 'emptyStateSvgPath.svg', + filteredSvgPath: 'emptyStateFilteredSvgPath.svg', + }); }); }); diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js index 5961babc5f8..2dd17888305 100644 --- a/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js +++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/ml_experiments_show_spec.js @@ -43,6 +43,8 @@ describe('MlExperimentsShow', () => { const hrefInRowAndColumn = (row, col) => findColumnInRow(row, col).findComponent(GlLink).attributes().href; + const linkTextInRowAndColumn = (row, col) => + findColumnInRow(row, col).findComponent(GlLink).text(); describe('default inputs', () => { beforeEach(() => { @@ -242,6 +244,7 @@ describe('MlExperimentsShow', () => { 'Rmse', 'Auc', 'Mae', + 'CI Job', 'Artifacts', ]; @@ -264,6 +267,26 @@ describe('MlExperimentsShow', () => { }); }); + describe('CI Job column', () => { + const jobColumnIndex = -2; + + it('has a link to the job', () => { + expect(hrefInRowAndColumn(firstCandidateIndex, jobColumnIndex)).toBe( + firstCandidate.ci_job.path, + ); + }); + + it('shows the name of the job', () => { + expect(linkTextInRowAndColumn(firstCandidateIndex, jobColumnIndex)).toBe( + firstCandidate.ci_job.name, + ); + }); + + it('shows empty state when there is no job', () => { + expect(findColumnInRow(secondCandidateIndex, jobColumnIndex).text()).toBe('-'); + }); + }); + describe('User column', () => { const userColumn = 2; diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js index adfb3dbf773..4a606be8da6 100644 --- a/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js +++ b/spec/frontend/ml/experiment_tracking/routes/experiments/show/mock_data.js @@ -15,6 +15,10 @@ export const MOCK_CANDIDATES = [ l1_ratio: 0.4, details: 'link_to_candidate1', artifact: 'link_to_artifact', + ci_job: { + path: 'link_to_job', + name: 'a job', + }, name: 'aCandidate', created_at: '2023-01-05T14:07:02.975Z', user: { username: 'root', path: '/root' }, diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js index 3d4f01d0da1..df10d33e2f0 100644 --- a/spec/frontend/security_configuration/mock_data.js +++ b/spec/frontend/security_configuration/mock_data.js @@ -9,10 +9,11 @@ import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants'; export const testProjectPath = 'foo/bar'; export const testProviderIds = [101, 102, 103]; -export const testProviderName = ['Kontra', 'Secure Code Warrior', 'Other Vendor']; +export const testProviderName = ['Kontra', 'Secure Code Warrior', 'SecureFlag']; export const testTrainingUrls = [ 'https://www.vendornameone.com/url', 'https://www.vendornametwo.com/url', + 'https://www.vendornamethree.com/url', ]; const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [ diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js index fbcf759d50f..739340f4936 100644 --- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js @@ -36,8 +36,8 @@ describe('Work item add note', () => { const createComponent = async ({ mutationHandler = mutationSuccessHandler, canUpdate = true, + workItemIid = '1', workItemResponse = workItemByIidResponseFactory({ canUpdate }), - queryVariables = { iid: '1' }, signedIn = true, isEditing = true, workItemType = 'Task', @@ -56,10 +56,12 @@ describe('Work item add note', () => { const { id } = workItemQueryResponse.data.workItem; wrapper = shallowMountExtended(WorkItemAddNote, { apolloProvider, + provide: { + fullPath: 'test-project-path', + }, propsData: { workItemId: id, - fullPath: 'test-project-path', - queryVariables, + workItemIid, workItemType, markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem', autocompleteDataSources: {}, @@ -114,11 +116,7 @@ describe('Work item add note', () => { }); it('emits `replied` event and hides form after successful mutation', async () => { - await createComponent({ - isEditing: true, - signedIn: true, - queryVariables: { iid: '1' }, - }); + await createComponent({ isEditing: true, signedIn: true }); findCommentForm().vm.$emit('submitForm', 'some text'); await waitForPromises(); @@ -226,8 +224,8 @@ describe('Work item add note', () => { expect(workItemResponseHandler).toHaveBeenCalled(); }); - it('skips calling the work item query when missing queryVariables', async () => { - await createComponent({ queryVariables: {}, isEditing: false }); + it('skips calling the work item query when missing workItemIid', async () => { + await createComponent({ workItemIid: null, isEditing: false }); expect(workItemResponseHandler).not.toHaveBeenCalled(); }); diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js index 28c8ea13a23..fac5011b6af 100644 --- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js @@ -28,16 +28,16 @@ describe('Work Item Discussion', () => { const createComponent = ({ discussion = [mockWorkItemCommentNote], workItemId = mockWorkItemId, - queryVariables = { iid: '1' }, - fullPath = 'gitlab-org', workItemType = 'Task', } = {}) => { wrapper = shallowMount(WorkItemDiscussion, { + provide: { + fullPath: 'gitlab-org', + }, propsData: { discussion, workItemId, - queryVariables, - fullPath, + workItemIid: '1', workItemType, markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem', autocompleteDataSources: {}, diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js index 956b52bb74c..f2cf5171cc1 100644 --- a/spec/frontend/work_items/components/notes/work_item_note_spec.js +++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js @@ -69,19 +69,20 @@ describe('Work Item Note', () => { workItemId = mockWorkItemId, updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler, assignees = mockAssignees, - queryVariables = { iid: '1' }, } = {}) => { wrapper = shallowMount(WorkItemNote, { + provide: { + fullPath: 'test-project-path', + }, propsData: { workItemId, + workItemIid: '1', note, isFirstNote, workItemType: 'Task', markdownPreviewPath: '/group/project/preview_markdown?target_type=WorkItem', autocompleteDataSources: {}, assignees, - queryVariables, - fullPath: 'test-project-path', }, apolloProvider: mockApollo([ [workItemByIidQuery, workItemResponseHandler], diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js index 1e336a928a0..25b0b74c217 100644 --- a/spec/frontend/work_items/components/work_item_assignees_spec.js +++ b/spec/frontend/work_items/components/work_item_assignees_spec.js @@ -82,6 +82,9 @@ describe('WorkItemAssignees component', () => { ]); wrapper = mountExtended(WorkItemAssignees, { + provide: { + fullPath: 'test-project-path', + }, propsData: { assignees, workItemId, @@ -89,7 +92,6 @@ describe('WorkItemAssignees component', () => { workItemType: TASK_TYPE_NAME, canUpdate, canInviteMembers, - fullPath: 'test-project-path', }, attachTo: document.body, apolloProvider, diff --git a/spec/frontend/work_items/components/work_item_created_updated_spec.js b/spec/frontend/work_items/components/work_item_created_updated_spec.js index 2a5b2853b5e..68ede7d5bc0 100644 --- a/spec/frontend/work_items/components/work_item_created_updated_spec.js +++ b/spec/frontend/work_items/components/work_item_created_updated_spec.js @@ -29,7 +29,10 @@ describe('WorkItemCreatedUpdated component', () => { wrapper = shallowMount(WorkItemCreatedUpdated, { apolloProvider: createMockApollo([[workItemByIidQuery, successHandler]]), - propsData: { workItemIid, fullPath: '/some/project' }, + provide: { + fullPath: '/some/project', + }, + propsData: { workItemIid }, stubs: { GlAvatarLink, GlSprintf, diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js index 174dd520a61..b7877784a2d 100644 --- a/spec/frontend/work_items/components/work_item_description_spec.js +++ b/spec/frontend/work_items/components/work_item_description_spec.js @@ -58,7 +58,7 @@ describe('WorkItemDescription', () => { canUpdate = true, workItemResponse = workItemByIidResponseFactory({ canUpdate }), isEditing = false, - queryVariables = { iid: '1' }, + workItemIid = '1', } = {}) => { workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse); @@ -71,10 +71,10 @@ describe('WorkItemDescription', () => { ]), propsData: { workItemId: id, - fullPath: 'test-project-path', - queryVariables, + workItemIid, }, provide: { + fullPath: 'test-project-path', glFeatures: { workItemsMvc, }, @@ -304,8 +304,8 @@ describe('WorkItemDescription', () => { expect(workItemResponseHandler).toHaveBeenCalled(); }); - it('skips calling the work item query when missing queryVariables', async () => { - await createComponent({ queryVariables: {} }); + it('skips calling the work item query when missing workItemIid', async () => { + await createComponent({ workItemIid: null }); expect(workItemResponseHandler).not.toHaveBeenCalled(); }); diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js index e6f7793b43f..554c9a4f7b8 100644 --- a/spec/frontend/work_items/components/work_item_labels_spec.js +++ b/spec/frontend/work_items/components/work_item_labels_spec.js @@ -46,7 +46,7 @@ describe('WorkItemLabels component', () => { workItemQueryHandler = workItemQuerySuccess, searchQueryHandler = successSearchQueryHandler, updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler, - queryVariables = { iid: '1' }, + workItemIid = '1', } = {}) => { wrapper = mountExtended(WorkItemLabels, { apolloProvider: createMockApollo([ @@ -55,11 +55,13 @@ describe('WorkItemLabels component', () => { [updateWorkItemMutation, updateWorkItemMutationHandler], [workItemLabelsSubscription, subscriptionHandler], ]), + provide: { + fullPath: 'test-project-path', + }, propsData: { workItemId, + workItemIid, canUpdate, - fullPath: 'test-project-path', - queryVariables, }, attachTo: document.body, }); @@ -263,8 +265,8 @@ describe('WorkItemLabels component', () => { expect(workItemQuerySuccess).toHaveBeenCalled(); }); - it('skips calling the work item query when missing queryVariables', async () => { - createComponent({ queryVariables: {} }); + it('skips calling the work item query when missing workItemIid', async () => { + createComponent({ workItemIid: null }); await waitForPromises(); expect(workItemQuerySuccess).not.toHaveBeenCalled(); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js index ad95350fc67..08b9408c656 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js @@ -28,12 +28,14 @@ describe('WorkItemChildrenWrapper', () => { } = {}) => { wrapper = shallowMountExtended(WorkItemChildrenWrapper, { apolloProvider: createMockApollo([[workItemByIidQuery, getWorkItemQueryHandler]]), + provide: { + fullPath: 'test/project', + }, propsData: { workItemType, workItemId: 'gid://gitlab/WorkItem/515', confidential, children, - projectPath: 'test/project', fetchByIid: true, }, }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js index adbb514d61a..bc429bfb037 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js @@ -52,7 +52,6 @@ describe('WorkItemLinkChild', () => { Vue.use(VueApollo); const createComponent = ({ - projectPath = 'gitlab-org/gitlab-test', canUpdate = true, issuableGid = WORK_ITEM_ID, childItem = workItemTask, @@ -71,8 +70,10 @@ describe('WorkItemLinkChild', () => { [getWorkItemTreeQuery, getWorkItemTreeQueryHandler], [updateWorkItemMutation, mutationChangeParentHandler], ]), + provide: { + fullPath: 'gitlab-org/gitlab-test', + }, propsData: { - projectPath, canUpdate, issuableGid, childItem, diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js index 6100bbea4a1..5f7f56d7063 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js @@ -61,7 +61,7 @@ describe('WorkItemLinksForm', () => { formType, }, provide: { - projectPath: 'project/path', + fullPath: 'project/path', hasIterationsFeature, }, }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index 0d726b0635a..cc3d2394231 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -76,7 +76,7 @@ describe('WorkItemLinks', () => { }; }, provide: { - projectPath: 'project/path', + fullPath: 'project/path', hasIterationsFeature, reportAbusePath: '/report/abuse/path', }, diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js index 710ed85bd97..06716584879 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js @@ -29,13 +29,15 @@ describe('WorkItemTree', () => { canUpdate = true, } = {}) => { wrapper = shallowMountExtended(WorkItemTree, { + provide: { + fullPath: 'test/project', + }, propsData: { workItemType, parentWorkItemType, workItemId: 'gid://gitlab/WorkItem/515', confidential, children, - projectPath: 'test/project', canUpdate, }, }); diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js index 95b0aee1315..c42c9a573e5 100644 --- a/spec/frontend/work_items/components/work_item_milestone_spec.js +++ b/spec/frontend/work_items/components/work_item_milestone_spec.js @@ -31,7 +31,6 @@ describe('WorkItemMilestone component', () => { const workItemId = 'gid://gitlab/WorkItem/1'; const workItemType = 'Task'; - const fullPath = 'full-path'; const findDropdown = () => wrapper.findComponent(GlDropdown); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); @@ -67,12 +66,14 @@ describe('WorkItemMilestone component', () => { [projectMilestonesQuery, searchQueryHandler], [updateWorkItemMutation, mutationHandler], ]), + provide: { + fullPath: 'full-path', + }, propsData: { canUpdate, workItemMilestone: milestone, workItemId, workItemType, - fullPath, }, stubs: { GlDropdown, diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js index be5861ba1e2..c2821cc99f9 100644 --- a/spec/frontend/work_items/components/work_item_notes_spec.js +++ b/spec/frontend/work_items/components/work_item_notes_spec.js @@ -97,11 +97,12 @@ describe('WorkItemNotes component', () => { [workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler], [workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler], ]), + provide: { + fullPath: 'test-path', + }, propsData: { workItemId, workItemIid, - queryVariables: { iid: '1' }, - fullPath: 'test-path', workItemType: 'task', reportAbusePath: '/report/abuse/path', isModal, @@ -139,6 +140,7 @@ describe('WorkItemNotes component', () => { await waitForPromises(); expect(workItemNotesQueryHandler).toHaveBeenCalledWith({ after: undefined, + fullPath: 'test-path', iid: '1', pageSize: 30, }); @@ -163,15 +165,17 @@ describe('WorkItemNotes component', () => { it('fetch more notes should be called', async () => { expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({ - pageSize: DEFAULT_PAGE_SIZE_NOTES, + fullPath: 'test-path', iid: '1', + pageSize: DEFAULT_PAGE_SIZE_NOTES, }); await nextTick(); expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({ - pageSize: DEFAULT_PAGE_SIZE_NOTES, + fullPath: 'test-path', iid: '1', + pageSize: DEFAULT_PAGE_SIZE_NOTES, after: mockMoreNotesWidgetResponse.discussions.pageInfo.endCursor, }); }); diff --git a/spec/helpers/projects/ml/experiments_helper_spec.rb b/spec/helpers/projects/ml/experiments_helper_spec.rb index 4296d6acdca..e0fbc8ac488 100644 --- a/spec/helpers/projects/ml/experiments_helper_spec.rb +++ b/spec/helpers/projects/ml/experiments_helper_spec.rb @@ -8,8 +8,16 @@ require 'mime/types' RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do let_it_be(:project) { create(:project, :private) } let_it_be(:experiment) { create(:ml_experiments, user_id: project.creator, project: project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:build) { create(:ci_build, pipeline: pipeline) } let_it_be(:candidate0) do - create(:ml_candidates, :with_artifact, experiment: experiment, user: project.creator, project: project).tap do |c| + create(:ml_candidates, + :with_artifact, + experiment: experiment, + user: project.creator, + project: project, + ci_build: build + ).tap do |c| c.params.build([{ name: 'param1', value: 'p1' }, { name: 'param2', value: 'p2' }]) c.metrics.create!( [{ name: 'metric1', value: 0.1 }, { name: 'metric2', value: 0.2 }, { name: 'metric3', value: 0.3 }] @@ -35,11 +43,13 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do { 'param1' => 'p1', 'param2' => 'p2', 'metric1' => '0.1000', 'metric2' => '0.2000', 'metric3' => '0.3000', 'artifact' => "/#{project.full_path}/-/packages/#{candidate0.artifact.id}", 'details' => "/#{project.full_path}/-/ml/candidates/#{candidate0.iid}", + 'ci_job' => { 'path' => "/#{project.full_path}/-/jobs/#{build.id}", 'name' => 'test' }, 'name' => candidate0.name, 'created_at' => candidate0.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), 'user' => { 'username' => candidate0.user.username, 'path' => "/#{candidate0.user.username}" } }, { 'param2' => 'p3', 'param3' => 'p4', 'metric3' => '0.4000', 'artifact' => nil, 'details' => "/#{project.full_path}/-/ml/candidates/#{candidate1.iid}", + 'ci_job' => nil, 'name' => candidate1.name, 'created_at' => candidate1.created_at.strftime('%Y-%m-%dT%H:%M:%S.%LZ'), 'user' => { 'username' => candidate1.user.username, 'path' => "/#{candidate1.user.username}" } } diff --git a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb index 0933f45e7c3..e14b1362687 100644 --- a/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb +++ b/spec/lib/banzai/filter/issuable_reference_expansion_filter_spec.rb @@ -21,6 +21,10 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor create(:issue, state, attributes.merge(project: project)) end + def create_item(issuable_type, state, attributes = {}) + create(issuable_type, state, attributes.merge(project: project)) + end + def create_merge_request(state, attributes = {}) create(:merge_request, state, attributes.merge(source_project: project, target_project: project)) end @@ -115,75 +119,88 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor end end - context 'for issue references' do - it 'ignores open issue references' do - issue = create_issue(:opened) - link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue') + shared_examples 'issue / work item references' do + it 'ignores open references' do + issuable = create_item(issuable_type, :opened) + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type) doc = filter(link, context) - expect(doc.css('a').last.text).to eq(issue.to_reference) + expect(doc.css('a').last.text).to eq(issuable.to_reference) end - it 'appends state to closed issue references' do - link = create_link(closed_issue.to_reference, issue: closed_issue.id, reference_type: 'issue') + it 'appends state to moved references' do + moved_issuable = create_item(issuable_type, :closed, project: project, + moved_to: create_item(issuable_type, :opened)) + link = create_link(moved_issuable.to_reference, "#{issuable_type}": moved_issuable.id, + reference_type: issuable_type) doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference} (closed)") + expect(doc.css('a').last.text).to eq("#{moved_issuable.to_reference} (moved)") end - it 'appends state to moved issue references' do - moved_issue = create(:issue, :closed, project: project, moved_to: create_issue(:opened)) - link = create_link(moved_issue.to_reference, issue: moved_issue.id, reference_type: 'issue') + it 'appends state to closed references' do + issuable = create_item(issuable_type, :closed) + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type) doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{moved_issue.to_reference} (moved)") + expect(doc.css('a').last.text).to eq("#{issuable.to_reference} (closed)") end it 'shows title for references with +' do - issue = create_issue(:opened, title: 'Some issue') - link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+') + issuable = create_item(issuable_type, :opened, title: 'Some issue') + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+') doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference})") + expect(doc.css('a').last.text).to eq("#{issuable.title} (#{issuable.to_reference})") end it 'truncates long title for references with +' do - issue = create_issue(:opened, title: 'Some issue ' * 10) - link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+') + issuable = create_item(issuable_type, :opened, title: 'Some issue ' * 10) + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+') doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{issue.title.truncate(50)} (#{issue.to_reference})") + expect(doc.css('a').last.text).to eq("#{issuable.title.truncate(50)} (#{issuable.to_reference})") end it 'shows both title and state for closed references with +' do - issue = create_issue(:closed, title: 'Some issue') - link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+') + issuable = create_item(issuable_type, :closed, title: 'Some issue') + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+') doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference} - closed)") + expect(doc.css('a').last.text).to eq("#{issuable.title} (#{issuable.to_reference} - closed)") end it 'shows title for references with +s' do - issue = create_issue(:opened, title: 'Some issue') - link = create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+s') + issuable = create_item(issuable_type, :opened, title: 'Some issue') + link = create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+s') doc = filter(link, context) - expect(doc.css('a').last.text).to eq("#{issue.title} (#{issue.to_reference}) • Unassigned") + expect(doc.css('a').last.text).to eq("#{issuable.title} (#{issuable.to_reference}) • Unassigned") end context 'when extended summary props are present' do let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:assignees) { create_list(:user, 3) } - let_it_be(:issue) { create_issue(:opened, title: 'Some issue', milestone: milestone, assignees: assignees) } + let_it_be(:issuable) do + create_item(issuable_type, :opened, title: 'Some issue', milestone: milestone, + assignees: assignees) + end + let_it_be(:link) do - create_link(issue.to_reference, issue: issue.id, reference_type: 'issue', reference_format: '+s') + create_link(issuable.to_reference, "#{issuable_type}": issuable.id, reference_type: issuable_type, + reference_format: '+s') end it 'shows extended summary for references with +s' do doc = filter(link, context) expect(doc.css('a').last.text).to eq( - "#{issue.title} (#{issue.to_reference}) • #{assignees[0].name}, #{assignees[1].name}+ • #{milestone.title}" + "#{issuable.title} (#{issuable.to_reference}) • #{assignees[0].name}, #{assignees[1].name}+ " \ + "• #{milestone.title}" ) end @@ -192,8 +209,10 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor let_it_be(:assignees2) { create_list(:user, 3) } it 'does not have N+1 for extended summary', :use_sql_query_cache do - issue2 = create_issue(:opened, title: 'Another issue', milestone: milestone2, assignees: assignees2) - link2 = create_link(issue2.to_reference, issue: issue2.id, reference_type: 'issue', reference_format: '+s') + issuable2 = create_item(issuable_type, :opened, title: 'Another issue', + milestone: milestone2, assignees: assignees2) + link2 = create_link(issuable2.to_reference, "#{issuable_type}": issuable2.id, + reference_type: issuable_type, reference_format: '+s') # warm up filter(link, context) @@ -212,6 +231,18 @@ RSpec.describe Banzai::Filter::IssuableReferenceExpansionFilter, feature_categor end end + context 'for work item references' do + let_it_be(:issuable_type) { :work_item } + + it_behaves_like 'issue / work item references' + end + + context 'for issue references' do + let_it_be(:issuable_type) { :issue } + + it_behaves_like 'issue / work item references' + end + context 'for merge request references' do it 'ignores open merge request references' do merge_request = create_merge_request(:opened) diff --git a/spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb b/spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb new file mode 100644 index 00000000000..e59e53891bf --- /dev/null +++ b/spec/lib/banzai/filter/references/work_item_reference_filter_spec.rb @@ -0,0 +1,314 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::Filter::References::WorkItemReferenceFilter, feature_category: :team_planning do + include FilterSpecHelper + + let_it_be(:namespace) { create(:namespace, name: 'main-namespace') } + let_it_be(:project) { create(:project, :public, namespace: namespace, path: 'main-project') } + let_it_be(:cross_namespace) { create(:namespace, name: 'cross-namespace') } + let_it_be(:cross_project) { create(:project, :public, namespace: cross_namespace, path: 'cross-project') } + let_it_be(:work_item) { create(:work_item, project: project) } + + def item_url(item) + work_item_path = "/#{item.project.namespace.path}/#{item.project.path}/-/work_items/#{item.iid}" + + "http://#{Gitlab.config.gitlab.host}#{work_item_path}" + end + + it 'subclasses from IssueReferenceFilter' do + expect(described_class.superclass).to eq Banzai::Filter::References::IssueReferenceFilter + end + + shared_examples 'a reference with work item type information' do + it 'contains work-item-type as a data attribute' do + doc = reference_filter("Fixed #{reference}") + + expect(doc.css('a').first.attr('data-work-item-type')).to eq('issue') + end + end + + shared_examples 'a work item reference' do + it_behaves_like 'a reference containing an element node' + + it_behaves_like 'a reference with work item type information' + + it 'links to a valid reference' do + doc = reference_filter("Fixed #{written_reference}") + + expect(doc.css('a').first.attr('href')).to eq work_item_url + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{written_reference}.)") + + expect(doc.text).to match(%r{^Fixed \(.*\.\)}) + end + + it 'includes a title attribute' do + doc = reference_filter("Issue #{written_reference}") + + expect(doc.css('a').first.attr('title')).to eq work_item.title + end + + it 'escapes the title attribute' do + work_item.update_attribute(:title, %("></a>whatever<a title=")) + + doc = reference_filter("Issue #{written_reference}") + + expect(doc.text).not_to include 'whatever' + end + + it 'renders non-HTML tooltips' do + doc = reference_filter("Issue #{written_reference}") + + expect(doc.at_css('a')).not_to have_attribute('data-html') + end + + it 'includes default classes' do + doc = reference_filter("Issue #{written_reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-work_item' + end + + it 'includes a data-project attribute' do + doc = reference_filter("Issue #{written_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq cross_project.id.to_s + end + + it 'includes a data-issue attribute' do + doc = reference_filter("See #{written_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-work-item') + expect(link.attr('data-work-item')).to eq work_item.id.to_s + end + + it 'includes data attributes for issuable popover' do + doc = reference_filter("See #{written_reference}") + link = doc.css('a').first + + expect(link.attr('data-project-path')).to eq cross_project.full_path + expect(link.attr('data-iid')).to eq work_item.iid.to_s + end + + it 'includes a data-original attribute' do + doc = reference_filter("See #{written_reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-original') + expect(link.attr('data-original')).to eq inner_text + end + + it 'does not escape the data-original attribute' do + skip if written_reference.start_with?('<a') + + inner_html = 'element <code>node</code> inside' + doc = reference_filter(%(<a href="#{written_reference}">#{inner_html}</a>)) + + expect(doc.children.first.attr('data-original')).to eq inner_html + end + + it 'includes a data-reference-format attribute' do + skip if written_reference.start_with?('<a') + + doc = reference_filter("Issue #{written_reference}+") + link = doc.css('a').first + + expect(link).to have_attribute('data-reference-format') + expect(link.attr('data-reference-format')).to eq('+') + expect(link.attr('href')).to eq(work_item_url) + end + + it 'includes a data-reference-format attribute for URL references' do + doc = reference_filter("Issue #{work_item_url}+") + link = doc.css('a').first + + expect(link).to have_attribute('data-reference-format') + expect(link.attr('data-reference-format')).to eq('+') + expect(link.attr('href')).to eq(work_item_url) + end + + it 'includes a data-reference-format attribute for extended summary URL references' do + doc = reference_filter("Issue #{work_item_url}+s") + link = doc.css('a').first + + expect(link).to have_attribute('data-reference-format') + expect(link.attr('data-reference-format')).to eq('+s') + expect(link.attr('href')).to eq(work_item_url) + end + + it 'does not process links containing issue numbers followed by text' do + href = "#{written_reference}st" + doc = reference_filter("<a href='#{href}'></a>") + link = doc.css('a').first.attr('href') + + expect(link).to eq(href) + end + end + + # Example: + # "See #1" + context 'when standard internal reference' do + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("Fixed ##{work_item.iid}") + + expect(doc.css('a')).to be_empty + end + end + + # Example: + # "See cross-namespace/cross-project#1" + context 'when cross-project / cross-namespace complete reference' do + let_it_be(:work_item2) { create(:work_item, project: cross_project) } + let_it_be(:reference) { "#{cross_project.full_path}##{work_item2.iid}" } + + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a')).to be_empty + end + end + + # Example: + # "See main-namespace/cross-project#1" + context 'when cross-project / same-namespace complete reference' do + let_it_be(:cross_project) { create(:project, :public, namespace: namespace, path: 'cross-project') } + let_it_be(:work_item) { create(:work_item, project: cross_project) } + let_it_be(:reference) { "#{cross_project.full_path}##{work_item.iid}" } + + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a')).to be_empty + end + end + + # Example: + # "See cross-project#1" + context 'when cross-project / same-namespace shorthand reference' do + let_it_be(:cross_project) { create(:project, :public, namespace: namespace, path: 'cross-project') } + let_it_be(:work_item) { create(:work_item, project: cross_project) } + let_it_be(:reference) { "#{cross_project.path}##{work_item.iid}" } + + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a')).to be_empty + end + end + + # Example: + # "See http://localhost/cross-namespace/cross-project/-/work_items/1" + context 'when cross-project URL reference' do + let_it_be(:work_item, reload: true) { create(:work_item, project: cross_project) } + let_it_be(:work_item_url) { item_url(work_item) } + let_it_be(:reference) { work_item_url } + let_it_be(:written_reference) { reference } + let_it_be(:inner_text) { written_reference } + + it_behaves_like 'a work item reference' + end + + # Example: + # "See http://localhost/cross-namespace/cross-project/-/work_items/1#note_123" + context 'when cross-project URL reference with comment anchor' do + let_it_be(:work_item) { create(:work_item, project: cross_project) } + let_it_be(:work_item_url) { item_url(work_item) } + let_it_be(:reference) { "#{work_item_url}#note_123" } + + it_behaves_like 'a reference containing an element node' + + it_behaves_like 'a reference with work item type information' + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq reference + end + + it 'link with trailing slash' do + doc = reference_filter("Fixed (#{work_item_url}/.)") + + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(work_item.to_reference(project))}</a>\.\)}) + end + + it 'links with adjacent text' do + doc = reference_filter("Fixed (#{reference}.)") + + expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(work_item.to_reference(project))} \(comment 123\)</a>\.\)}) + end + end + + # Example: + # 'See <a href="cross-namespace/cross-project#1">Reference</a>'' + context 'when cross-project reference in link href' do + let_it_be(:work_item) { create(:work_item, project: cross_project) } + let_it_be(:reference) { work_item.to_reference(project) } + let_it_be(:reference_link) { %(<a href="#{reference}">Reference</a>) } + let_it_be(:work_item_url) { item_url(work_item) } + + it 'is handled by IssueReferenceFilter, not WorkItemReferenceFilter' do + doc = reference_filter("See #{reference_link}") + + expect(doc.css('a').first[:href]).to eq reference + expect(doc.css('a').first[:href]).not_to eq work_item_url + end + end + + # Example: + # 'See <a href=\"http://localhost/cross-namespace/cross-project/-/work_items/1\">Reference</a>'' + context 'when cross-project URL in link href' do + let_it_be(:work_item, reload: true) { create(:work_item, project: cross_project) } + let_it_be(:work_item_url) { item_url(work_item) } + let_it_be(:reference) { work_item_url } + let_it_be(:reference_link) { %(<a href="#{reference}">Reference</a>) } + let_it_be(:written_reference) { reference_link } + let_it_be(:inner_text) { 'Reference' } + + it_behaves_like 'a work item reference' + end + + context 'for group context' do + let_it_be(:group) { create(:group) } + let_it_be(:context) { { project: nil, group: group } } + let_it_be(:work_item_url) { item_url(work_item) } + + it 'links to a valid reference for url cross-namespace' do + reference = "#{work_item_url}#note_123" + + doc = reference_filter("See #{reference}", context) + + link = doc.css('a').first + expect(link.attr('href')).to eq("#{work_item_url}#note_123") + expect(link.text).to include("#{project.full_path}##{work_item.iid}") + end + + it 'links to a valid reference for cross-namespace in link href' do + reference = "#{work_item_url}#note_123" + reference_link = %(<a href="#{reference}">Reference</a>) + + doc = reference_filter("See #{reference_link}", context) + + link = doc.css('a').first + expect(link.attr('href')).to eq("#{work_item_url}#note_123") + expect(link.text).to include('Reference') + end + end + + describe 'performance' do + let(:another_work_item) { create(:work_item, project: project) } + + it 'does not have a N+1 query problem' do + single_reference = "Work item #{work_item.to_reference}" + multiple_references = "Work items #{work_item.to_reference} and #{another_work_item.to_reference}" + + control_count = ActiveRecord::QueryRecorder.new { reference_filter(single_reference).to_html }.count + + expect { reference_filter(multiple_references).to_html }.not_to exceed_query_limit(control_count) + end + end +end diff --git a/spec/lib/banzai/issuable_extractor_spec.rb b/spec/lib/banzai/issuable_extractor_spec.rb index b2c869bd066..5bbd98592e7 100644 --- a/spec/lib/banzai/issuable_extractor_spec.rb +++ b/spec/lib/banzai/issuable_extractor_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do let(:user) { create(:user) } let(:extractor) { described_class.new(Banzai::RenderContext.new(project, user)) } let(:issue) { create(:issue, project: project) } + let(:work_item) { create(:work_item, project: project) } let(:merge_request) { create(:merge_request, source_project: project) } let(:issue_link) do html_to_node( @@ -14,6 +15,12 @@ RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do ) end + let(:work_item_link) do + html_to_node( + "<a href='' data-work-item='#{work_item.id}' data-reference-type='work_item' class='gfm'>text</a>" + ) + end + let(:merge_request_link) do html_to_node( "<a href='' data-merge-request='#{merge_request.id}' data-reference-type='merge_request' class='gfm'>text</a>" @@ -27,17 +34,17 @@ RSpec.describe Banzai::IssuableExtractor, feature_category: :team_planning do end it 'returns instances of issuables for nodes with references' do - result = extractor.extract([issue_link, merge_request_link]) + result = extractor.extract([issue_link, work_item_link, merge_request_link]) - expect(result).to eq(issue_link => issue, merge_request_link => merge_request) + expect(result).to eq(issue_link => issue, work_item_link => work_item, merge_request_link => merge_request) end describe 'caching', :request_store do it 'saves records to cache' do - extractor.extract([issue_link, merge_request_link]) + extractor.extract([issue_link, work_item_link, merge_request_link]) second_call_queries = ActiveRecord::QueryRecorder.new do - extractor.extract([issue_link, merge_request_link]) + extractor.extract([issue_link, work_item_link, merge_request_link]) end.count expect(second_call_queries).to eq 0 diff --git a/spec/lib/banzai/reference_parser/work_item_parser_spec.rb b/spec/lib/banzai/reference_parser/work_item_parser_spec.rb new file mode 100644 index 00000000000..dbde01cc94f --- /dev/null +++ b/spec/lib/banzai/reference_parser/work_item_parser_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Banzai::ReferenceParser::WorkItemParser, feature_category: :team_planning do + include ReferenceParserHelpers + + let_it_be(:group) { create(:group, :public) } + let_it_be_with_reload(:project) { create(:project, :public, group: group) } + let_it_be(:user) { create(:user) } + let_it_be(:work_item) { create(:work_item, project: project) } + let_it_be(:link) { empty_html_link } + + subject { described_class.new(Banzai::RenderContext.new(project, user)) } + + describe '#records_for_nodes' do + it 'returns a Hash containing the work items for a list of nodes' do + link['data-work-item'] = work_item.id.to_s + nodes = [link] + + expect(subject.records_for_nodes(nodes)).to eq({ link => work_item }) + end + end + + context 'when checking multiple work items on another project' do + let_it_be(:other_project) { create(:project, :public) } + let_it_be(:other_work_item) { create(:work_item, project: other_project) } + let_it_be(:control_links) do + [work_item_link(other_work_item)] + end + + let_it_be(:actual_links) do + control_links + [work_item_link(create(:work_item, project: other_project))] + end + + def work_item_link(work_item) + Nokogiri::HTML.fragment(%(<a data-work-item="#{work_item.id}"></a>)).children[0] + end + + before do + project.add_developer(user) + end + + it_behaves_like 'no N+1 queries' + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index d7dcfe64c74..a35dd968cd6 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -2414,9 +2414,33 @@ module Gitlab end context 'duplicate needs' do - let(:needs) { %w(build1 build1) } + context 'when needs are specified in an array' do + let(:needs) { %w(build1 build1) } - it_behaves_like 'returns errors', 'test1 has duplicate entries in the needs section.' + it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build1.' + end + + context 'when a job is specified multiple times' do + let(:needs) do + [ + { job: "build2", artifacts: true, optional: false }, + { job: "build2", artifacts: true, optional: false } + ] + end + + it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build2.' + end + + context 'when job is specified multiple times with different attributes' do + let(:needs) do + [ + { job: "build2", artifacts: false, optional: true }, + { job: "build2", artifacts: true, optional: false } + ] + end + + it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build2.' + end end context 'needs and dependencies that are mismatching' do diff --git a/spec/migrations/20230328030101_add_secureflag_training_provider_spec.rb b/spec/migrations/20230328030101_add_secureflag_training_provider_spec.rb new file mode 100644 index 00000000000..774ea89937a --- /dev/null +++ b/spec/migrations/20230328030101_add_secureflag_training_provider_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddSecureflagTrainingProvider, :migration, feature_category: :vulnerability_management do + include MigrationHelpers::WorkItemTypesHelper + + let!(:security_training_providers) { table(:security_training_providers) } + + it 'adds additional provider' do + # Need to delete all as security training providers are seeded before entire test suite + security_training_providers.delete_all + + reversible_migration do |migration| + migration.before -> { + expect(security_training_providers.count).to eq(0) + } + + migration.after -> { + expect(security_training_providers.count).to eq(1) + } + end + end +end diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb index a39babd78b7..fa19b723ee2 100644 --- a/spec/models/ml/candidate_spec.rb +++ b/spec/models/ml/candidate_spec.rb @@ -157,6 +157,9 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d expect(subject.association_cached?(:latest_metrics)).to be(true) expect(subject.association_cached?(:params)).to be(true) expect(subject.association_cached?(:user)).to be(true) + expect(subject.association_cached?(:project)).to be(true) + expect(subject.association_cached?(:package)).to be(true) + expect(subject.association_cached?(:ci_build)).to be(true) end end @@ -188,6 +191,22 @@ RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops d end end + describe 'from_ci?' do + subject { candidate } + + it 'is false if candidate does not have ci_build_id' do + allow(candidate).to receive(:ci_build_id).and_return(nil) + + is_expected.not_to be_from_ci + end + + it 'is true if candidate does has ci_build_id' do + allow(candidate).to receive(:ci_build_id).and_return(1) + + is_expected.to be_from_ci + end + end + describe '#order_by_metric' do let_it_be(:auc_metrics) do create(:ml_candidate_metrics, name: 'auc', value: 0.4, candidate: candidate) diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb index 57193d4ac49..24920421ae6 100644 --- a/spec/models/work_item_spec.rb +++ b/spec/models/work_item_spec.rb @@ -329,6 +329,20 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do end end + describe '#link_reference_pattern' do + let(:match_data) { described_class.link_reference_pattern.match(link_reference_url) } + + context 'with work item url' do + let(:link_reference_url) { 'http://localhost/namespace/project/-/work_items/1' } + + it 'matches with expected attributes' do + expect(match_data['namespace']).to eq('namespace') + expect(match_data['project']).to eq('project') + expect(match_data['work_item']).to eq('1') + end + end + end + context 'with hierarchy' do let_it_be(:type1) { create(:work_item_type, namespace: reusable_project.namespace) } let_it_be(:type2) { create(:work_item_type, namespace: reusable_project.namespace) } diff --git a/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb b/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb new file mode 100644 index 00000000000..d2f5e4aa619 --- /dev/null +++ b/spec/rubocop/cop/rspec/avoid_conditional_statements_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' + +require_relative '../../../../rubocop/cop/rspec/avoid_conditional_statements' + +RSpec.describe RuboCop::Cop::RSpec::AvoidConditionalStatements, feature_category: :tooling do + context 'when using conditionals' do + it 'flags if conditional' do + expect_offense(<<~RUBY) + if page.has_css?('[data-testid="begin-commit-button"]') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `if` conditional statments in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109 + find('[data-testid="begin-commit-button"]').click + end + RUBY + end + + it 'flags unless conditional' do + expect_offense(<<~RUBY) + RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_ide do + it 'creates directory in current directory' do + unless page.has_css?('[data-testid="begin-commit-button"]') + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `unless` conditional statments in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109 + find('[data-testid="begin-commit-button"]').click + end + end + end + RUBY + end + + it 'flags ternary operator' do + expect_offense(<<~RUBY) + RSpec.describe 'Multi-file editor new directory', :js, feature_category: :web_ide do + it 'creates directory in current directory' do + user.present ? user : nil + ^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use `(send (send nil :user) :present) ? (ternary)` conditional statments in specs, it might create flakiness. See https://gitlab.com/gitlab-org/gitlab/-/issues/385304#note_1345437109 + end + end + RUBY + end + end +end diff --git a/spec/support/capybara_wait_for_all_requests_after_visit_page.rb b/spec/support/capybara_wait_for_all_requests.rb index f20e82e89a9..86f3e77cc19 100644 --- a/spec/support/capybara_wait_for_all_requests_after_visit_page.rb +++ b/spec/support/capybara_wait_for_all_requests.rb @@ -18,4 +18,22 @@ module Capybara prepend WaitForAllRequestsAfterVisitPage end + + module Node + module Actions + include CapybaraHelpers + include WaitHelpers + include WaitForRequests + + module WaitForAllRequestsAfterClickButton + def click_button(locator = nil, **options) + super + + wait_for_all_requests + end + end + + prepend WaitForAllRequestsAfterClickButton + end + end end diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml index 0e94c5e348e..8fcb4ee7b9c 100644 --- a/spec/support/finder_collection_allowlist.yml +++ b/spec/support/finder_collection_allowlist.yml @@ -63,6 +63,7 @@ - Security::TrainingUrlsFinder - Security::TrainingProviders::KontraUrlFinder - Security::TrainingProviders::SecureCodeWarriorUrlFinder +- Security::TrainingProviders::SecureFlagUrlFinder - SentryIssueFinder - ServerlessDomainFinder - TagsFinder diff --git a/spec/support/helpers/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb index 0cb2863dc2c..5d9ef557ae6 100644 --- a/spec/support/helpers/markdown_feature.rb +++ b/spec/support/helpers/markdown_feature.rb @@ -48,6 +48,10 @@ class MarkdownFeature @issue ||= create(:issue, project: project) end + def work_item + @issue ||= create(:work_item, project: project) + end + def merge_request @merge_request ||= create(:merge_request, :simple, source_project: project) end @@ -106,6 +110,10 @@ class MarkdownFeature @xissue ||= create(:issue, project: xproject) end + def xwork_item + @xwork_item ||= create(:work_item, project: xproject) + end + def xmerge_request @xmerge_request ||= create(:merge_request, :simple, source_project: xproject) end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 575ae572f25..8fdece7b26d 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -115,7 +115,16 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_selector('a.gfm.gfm-issue', count: 6) + expect(actual).to have_selector('a.gfm.gfm-issue', count: 9) + end + end + + # WorkItemReferenceFilter + matcher :reference_work_items do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-work_item', count: 2) end end diff --git a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb index cb74d0e8dca..9c096c5a158 100644 --- a/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb +++ b/spec/support/shared_examples/analytics/cycle_analytics/flow_metrics_examples.rb @@ -498,3 +498,132 @@ RSpec.shared_examples 'value stream analytics flow metrics cycleTime examples' d end end end + +RSpec.shared_examples 'value stream analytics flow metrics issuesCompleted examples' do + let_it_be(:milestone) { create(:milestone, group: group) } + let_it_be(:label) { create(:group_label, group: group) } + + let_it_be(:author) { create(:user) } + let_it_be(:assignee) { create(:user) } + + # we don't care about opened date, only closed date. + let_it_be(:issue1) do + create(:issue, project: project1, author: author, created_at: 17.days.ago, closed_at: 12.days.ago) + end + + let_it_be(:issue2) do + create(:issue, project: project2, author: author, created_at: 16.days.ago, closed_at: 13.days.ago) + end + + let_it_be(:issue3) do + create(:labeled_issue, + project: project1, + labels: [label], + author: author, + milestone: milestone, + assignees: [assignee], + created_at: 14.days.ago, + closed_at: 11.days.ago) + end + + let_it_be(:issue4) do + create(:labeled_issue, + project: project2, + labels: [label], + assignees: [assignee], + created_at: 20.days.ago, + closed_at: 15.days.ago) + end + + before do + Analytics::CycleAnalytics::DataLoaderService.new(group: group, model: Issue).execute + end + + let(:query) do + <<~QUERY + query($path: ID!, $assigneeUsernames: [String!], $authorUsername: String, $milestoneTitle: String, $labelNames: [String!], $from: Time!, $to: Time!) { + #{context}(fullPath: $path) { + flowMetrics { + issuesCompletedCount(assigneeUsernames: $assigneeUsernames, authorUsername: $authorUsername, milestoneTitle: $milestoneTitle, labelNames: $labelNames, from: $from, to: $to) { + value + unit + identifier + title + links { + label + url + } + } + } + } + } + QUERY + end + + let(:variables) do + { + path: full_path, + from: 21.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + subject(:result) do + post_graphql(query, current_user: current_user, variables: variables) + + graphql_data.dig(context.to_s, 'flowMetrics', 'issuesCompletedCount') + end + + it 'returns the correct value' do + expect(result).to match(a_hash_including({ + 'identifier' => 'issues_completed', + 'unit' => n_('issue', 'issues', 4), + 'value' => 4, + 'title' => _('Issues Completed'), + 'links' => [ + { 'label' => s_('ValueStreamAnalytics|Dashboard'), 'url' => match(/issues_analytics/) }, + { 'label' => s_('ValueStreamAnalytics|Go to docs'), 'url' => match(/definitions/) } + ] + })) + end + + context 'when the user is not authorized' do + let(:current_user) { create(:user) } + + it 'returns nil' do + expect(result).to eq(nil) + end + end + + context 'when outside of the date range' do + let(:variables) do + { + path: full_path, + from: 30.days.ago.iso8601, + to: 25.days.ago.iso8601 + } + end + + it 'returns 0 count' do + expect(result).to match(a_hash_including({ 'value' => 0.0 })) + end + end + + context 'with all filters' do + let(:variables) do + { + path: full_path, + assigneeUsernames: [assignee.username], + labelNames: [label.title], + authorUsername: author.username, + milestoneTitle: milestone.title, + from: 20.days.ago.iso8601, + to: 10.days.ago.iso8601 + } + end + + it 'returns filtered count' do + expect(result).to match(a_hash_including({ 'value' => 1.0 })) + end + end +end diff --git a/spec/support/shared_examples/security_training_providers_importer.rb b/spec/support/shared_examples/security_training_providers_importer.rb index 69d92964270..81b3d22ab23 100644 --- a/spec/support/shared_examples/security_training_providers_importer.rb +++ b/spec/support/shared_examples/security_training_providers_importer.rb @@ -8,7 +8,7 @@ RSpec.shared_examples 'security training providers importer' do end it 'upserts security training providers' do - expect { 2.times { subject } }.to change { security_training_providers.count }.from(0).to(2) - expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior']) + expect { 3.times { subject } }.to change { security_training_providers.count }.from(0).to(3) + expect(security_training_providers.all.map(&:name)).to match_array(['Kontra', 'Secure Code Warrior', 'SecureFlag']) end end diff --git a/spec/support_specs/capybara_wait_for_all_requests_after_page_visit_spec.rb b/spec/support_specs/capybara_wait_for_all_requests_after_page_visit_spec.rb deleted file mode 100644 index aec67b66379..00000000000 --- a/spec/support_specs/capybara_wait_for_all_requests_after_page_visit_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'fast_spec_helper' -require 'capybara' -require 'support/capybara_wait_for_all_requests_after_visit_page' - -RSpec.describe Capybara::Session::WaitForAllRequestsAfterVisitPage, feature_category: :tooling do # rubocop:disable RSpec/FilePath - let(:page_visitor) do - Class.new do - def visit(visit_uri) - visit_uri - end - - prepend Capybara::Session::WaitForAllRequestsAfterVisitPage - end.new - end - - it 'waits for all requests after a page visit' do - expect(page_visitor).to receive(:wait_for_all_requests) - - page_visitor.visit('http://test.com') - end -end diff --git a/spec/support_specs/capybara_wait_for_all_requests_spec.rb b/spec/support_specs/capybara_wait_for_all_requests_spec.rb new file mode 100644 index 00000000000..fd105c3ab01 --- /dev/null +++ b/spec/support_specs/capybara_wait_for_all_requests_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'capybara' +require 'support/capybara_wait_for_all_requests' + +RSpec.describe 'capybara_wait_for_all_requests', feature_category: :tooling do # rubocop:disable RSpec/FilePath + context 'for Capybara::Session::WaitForAllRequestsAfterVisitPage' do + let(:page_visitor) do + Class.new do + def visit(visit_uri) + visit_uri + end + + prepend Capybara::Session::WaitForAllRequestsAfterVisitPage + end.new + end + + it 'waits for all requests after a page visit' do + expect(page_visitor).to receive(:wait_for_all_requests) + + page_visitor.visit('http://test.com') + end + end + + context 'for Capybara::Node::Actions::WaitForAllRequestsAfterClickButton' do + let(:node) do + Class.new do + def click_button(locator = nil, **_options) + locator + end + + prepend Capybara::Node::Actions::WaitForAllRequestsAfterClickButton + end.new + end + + it 'waits for all requests after a page visit' do + expect(node).to receive(:wait_for_all_requests) + + node.click_button + end + end +end |