summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop_todo/gitlab/doc_url.yml1
-rw-r--r--.rubocop_todo/layout/argument_alignment.yml1
-rw-r--r--.rubocop_todo/layout/empty_line_after_magic_comment.yml1
-rw-r--r--.rubocop_todo/layout/line_continuation_leading_space.yml1
-rw-r--r--.rubocop_todo/layout/line_end_string_concatenation_indentation.yml1
-rw-r--r--.rubocop_todo/lint/symbol_conversion.yml8
-rw-r--r--.rubocop_todo/rspec/missing_feature_category.yml1
-rw-r--r--CHANGELOG.md46
-rw-r--r--Gemfile2
-rw-r--r--app/assets/javascripts/admin/abuse_reports/components/abuse_report_details.vue66
-rw-r--r--app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue49
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue9
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue6
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/constants.js1
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue31
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue2
-rw-r--r--app/assets/javascripts/labels/label_manager.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js5
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue2
-rw-r--r--app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue28
-rw-r--r--app/assets/javascripts/token_access/components/outbound_token_access.vue49
-rw-r--r--app/assets/javascripts/token_access/components/token_access_app.vue1
-rw-r--r--app/assets/javascripts/token_access/index.js1
-rw-r--r--app/assets/stylesheets/pages/labels.scss18
-rw-r--r--app/controllers/concerns/search_rate_limitable.rb18
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb2
-rw-r--r--app/helpers/avatars_helper.rb2
-rw-r--r--app/models/merge_request.rb5
-rw-r--r--app/models/repository.rb12
-rw-r--r--app/serializers/admin/abuse_report_entity.rb31
-rw-r--r--app/services/integrations/slack_event_service.rb61
-rw-r--r--app/services/integrations/slack_events/app_home_opened_service.rb92
-rw-r--r--app/services/integrations/slack_events/url_verification_service.rb26
-rw-r--r--app/services/integrations/slack_interaction_service.rb36
-rw-r--r--app/services/integrations/slack_interactions/block_action_service.rb32
-rw-r--r--app/services/integrations/slack_interactions/incident_management/incident_modal_closed_service.rb58
-rw-r--r--app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb162
-rw-r--r--app/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler.rb131
-rw-r--r--app/views/groups/labels/index.html.haml12
-rw-r--r--app/views/projects/labels/index.html.haml34
-rw-r--r--app/views/shared/_label.html.haml98
-rw-r--r--app/views/shared/_label_full_path.html.haml6
-rw-r--r--app/views/shared/_label_row.html.haml35
-rw-r--r--app/views/shared/empty_states/_priority_labels.html.haml8
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/integrations/slack_event_worker.rb54
-rw-r--r--config/feature_flags/development/search_rate_limited_scopes.yml8
-rw-r--r--data/deprecations/15-8-pull-through-cache-container-registry.yml2
-rw-r--r--data/removals/16_0/16-0-azure-storage-driver-registry.yml12
-rw-r--r--db/post_migrate/20230420002547_swap_todos_note_id_to_bigint_for_gitlab_dot_com.rb63
-rw-r--r--db/post_migrate/20230422013640_swap_system_note_metadata_note_id_to_bigint_for_gitlab_dot_com.rb69
-rw-r--r--db/schema_migrations/202304200025471
-rw-r--r--db/schema_migrations/202304220136401
-rw-r--r--db/structure.sql18
-rw-r--r--doc/administration/geo/replication/troubleshooting.md6
-rw-r--r--doc/administration/instance_limits.md9
-rw-r--r--doc/administration/silent_mode/index.md2
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/graphql/removed_items.md16
-rw-r--r--doc/ci/pipeline_editor/index.md11
-rw-r--r--doc/ci/troubleshooting.md2
-rw-r--r--doc/development/gemfile.md7
-rw-r--r--doc/development/testing_guide/flaky_tests.md2
-rw-r--r--doc/integration/saml.md2
-rw-r--r--doc/subscriptions/customers_portal.md7
-rw-r--r--doc/subscriptions/gitlab_com/index.md16
-rw-r--r--doc/update/deprecations.md2
-rw-r--r--doc/update/removals.md10
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md2
-rw-r--r--doc/user/project/description_templates.md14
-rw-r--r--doc/user/project/merge_requests/creating_merge_requests.md2
-rw-r--r--doc/user/project/merge_requests/index.md16
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/integrations/slack/events.rb58
-rw-r--r--lib/api/integrations/slack/interactions.rb30
-rw-r--r--lib/api/integrations/slack/options.rb2
-rw-r--r--lib/banzai/filter/asset_proxy_filter.rb44
-rw-r--r--lib/banzai/filter/commit_trailers_filter.rb2
-rw-r--r--lib/gitlab/checks/branch_check.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/database/dynamic_model_helpers.rb20
-rw-r--r--lib/gitlab_settings.rb6
-rw-r--r--lib/gitlab_settings/settings.rb9
-rw-r--r--lib/slack/block_kit/app_home_opened.rb173
-rw-r--r--locale/gitlab.pot100
-rw-r--r--package.json2
-rw-r--r--qa/qa/page/project/pipeline_editor/show.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb4
-rw-r--r--spec/controllers/search_controller_spec.rb100
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb45
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb2
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_details_spec.js53
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js67
-rw-r--r--spec/frontend/admin/abuse_reports/mock_data.js18
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js42
-rw-r--r--spec/frontend/jobs/components/table/job_table_app_spec.js10
-rw-r--r--spec/frontend/merge_request_tabs_spec.js4
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js10
-rw-r--r--spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js32
-rw-r--r--spec/frontend/token_access/outbound_token_access_spec.js68
-rw-r--r--spec/helpers/avatars_helper_spec.rb16
-rw-r--r--spec/lib/banzai/filter/asset_proxy_filter_spec.rb9
-rw-r--r--spec/lib/banzai/filter/commit_trailers_filter_spec.rb23
-rw-r--r--spec/lib/gitlab/checks/branch_check_spec.rb8
-rw-r--r--spec/lib/gitlab/database/dynamic_model_helpers_spec.rb44
-rw-r--r--spec/lib/gitlab_settings/settings_spec.rb36
-rw-r--r--spec/lib/slack/block_kit/app_home_opened_spec.rb62
-rw-r--r--spec/migrations/swap_system_note_metadata_note_id_to_bigint_for_gitlab_dot_com_spec.rb66
-rw-r--r--spec/migrations/swap_todos_note_id_to_bigint_for_gitlab_dot_com_spec.rb66
-rw-r--r--spec/models/merge_request_spec.rb9
-rw-r--r--spec/models/namespace/package_setting_spec.rb52
-rw-r--r--spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb3
-rw-r--r--spec/models/repository_spec.rb12
-rw-r--r--spec/requests/api/integrations/slack/events_spec.rb91
-rw-r--r--spec/requests/api/integrations/slack/interactions_spec.rb69
-rw-r--r--spec/serializers/admin/abuse_report_entity_spec.rb59
-rw-r--r--spec/services/draft_notes/publish_service_spec.rb7
-rw-r--r--spec/services/integrations/slack_event_service_spec.rb56
-rw-r--r--spec/services/integrations/slack_events/app_home_opened_service_spec.rb113
-rw-r--r--spec/services/integrations/slack_events/url_verification_service_spec.rb11
-rw-r--r--spec/services/integrations/slack_interaction_service_spec.rb70
-rw-r--r--spec/services/integrations/slack_interactions/block_action_service_spec.rb48
-rw-r--r--spec/services/integrations/slack_interactions/incident_management/incident_modal_closed_service_spec.rb78
-rw-r--r--spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb296
-rw-r--r--spec/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler_spec.rb158
-rw-r--r--spec/services/merge_requests/reload_diffs_service_spec.rb5
-rw-r--r--spec/views/shared/_label_row.html.haml_spec.rb4
-rw-r--r--spec/workers/integrations/slack_event_worker_spec.rb129
-rw-r--r--vendor/gems/bundler-checksum/README.md2
-rw-r--r--vendor/gems/bundler-checksum/test/project_with_checksum_lock/Gemfile2
-rw-r--r--vendor/project_templates/express.tar.gzbin17923 -> 28962 bytes
-rw-r--r--workhorse/internal/headers/content_headers.go32
-rw-r--r--workhorse/internal/headers/content_headers_test.go56
-rw-r--r--workhorse/internal/senddata/contentprocessor/contentprocessor_test.go4
-rw-r--r--workhorse/testdata/index.xhtml9
-rw-r--r--workhorse/testdata/test.xml6
-rw-r--r--workhorse/testdata/xml.svg7
-rw-r--r--yarn.lock8
143 files changed, 3550 insertions, 654 deletions
diff --git a/.rubocop_todo/gitlab/doc_url.yml b/.rubocop_todo/gitlab/doc_url.yml
index 67c70593b44..ad203c8abd3 100644
--- a/.rubocop_todo/gitlab/doc_url.yml
+++ b/.rubocop_todo/gitlab/doc_url.yml
@@ -24,7 +24,6 @@ Gitlab/DocUrl:
- 'ee/app/mailers/emails/user_cap.rb'
- 'ee/app/workers/concerns/elastic/migration_obsolete.rb'
- 'ee/lib/ee/gitlab/ci/pipeline/quota/size.rb'
- - 'ee/lib/slack/block_kit/app_home_opened.rb'
- 'ee/lib/system_check/app/advanced_search_migrations_check.rb'
- 'ee/lib/tasks/gitlab/geo.rake'
- 'lib/backup/database.rb'
diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml
index 48a2b52e908..a7d176a4fc1 100644
--- a/.rubocop_todo/layout/argument_alignment.yml
+++ b/.rubocop_todo/layout/argument_alignment.yml
@@ -1145,7 +1145,6 @@ Layout/ArgumentAlignment:
- 'ee/lib/gitlab/status_page/pipeline/post_process_pipeline.rb'
- 'ee/lib/gitlab/subscription_portal/clients/graphql.rb'
- 'ee/lib/gitlab/zoekt/search_results.rb'
- - 'ee/lib/slack/block_kit/app_home_opened.rb'
- 'ee/spec/components/billing/plan_component_spec.rb'
- 'ee/spec/components/namespaces/storage/pre_enforcement_alert_component_spec.rb'
- 'ee/spec/elastic/migrate/20220119120500_populate_commit_permissions_in_main_index_spec.rb'
diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
index 191c581da1b..4ba1efe4916 100644
--- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml
+++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
@@ -425,7 +425,6 @@ Layout/EmptyLineAfterMagicComment:
- 'ee/spec/services/wikis/create_attachment_service_spec.rb'
- 'ee/spec/support/helpers/board_helpers.rb'
- 'ee/spec/workers/app_sec/dast/profile_schedule_worker_spec.rb'
- - 'ee/spec/workers/integrations/slack_event_worker_spec.rb'
- 'ee/spec/workers/namespaces/free_user_cap/backfill_notification_jobs_worker_spec.rb'
- 'lib/api/commits.rb'
- 'lib/api/concerns/packages/nuget_endpoints.rb'
diff --git a/.rubocop_todo/layout/line_continuation_leading_space.yml b/.rubocop_todo/layout/line_continuation_leading_space.yml
index ca9a5a0b6fb..f2c9feec08d 100644
--- a/.rubocop_todo/layout/line_continuation_leading_space.yml
+++ b/.rubocop_todo/layout/line_continuation_leading_space.yml
@@ -20,7 +20,6 @@ Layout/LineContinuationLeadingSpace:
- 'ee/app/services/system_notes/epics_service.rb'
- 'ee/lib/ee/gitlab/ci/pipeline/quota/size.rb'
- 'ee/lib/ee/gitlab/git_access.rb'
- - 'ee/lib/slack/block_kit/app_home_opened.rb'
- 'ee/lib/tasks/gitlab/geo.rake'
- 'ee/spec/features/epic_boards/epic_boards_sidebar_spec.rb'
- 'ee/spec/features/gitlab_subscriptions/seat_count_alert_spec.rb'
diff --git a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
index 826671ab30a..29d28b2006a 100644
--- a/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
+++ b/.rubocop_todo/layout/line_end_string_concatenation_indentation.yml
@@ -81,7 +81,6 @@ Layout/LineEndStringConcatenationIndentation:
- 'ee/lib/ee/gitlab/git_access.rb'
- 'ee/lib/ee/gitlab/namespace_storage_size_error_message.rb'
- 'ee/lib/gitlab/manual_quarterly_co_term_banner.rb'
- - 'ee/lib/slack/block_kit/app_home_opened.rb'
- 'ee/lib/tasks/gitlab/geo.rake'
- 'ee/spec/controllers/admin/licenses_controller_spec.rb'
- 'ee/spec/controllers/groups/group_members_controller_spec.rb'
diff --git a/.rubocop_todo/lint/symbol_conversion.yml b/.rubocop_todo/lint/symbol_conversion.yml
index aa6d96190b3..c7f8b9549fb 100644
--- a/.rubocop_todo/lint/symbol_conversion.yml
+++ b/.rubocop_todo/lint/symbol_conversion.yml
@@ -15,15 +15,11 @@ Lint/SymbolConversion:
- 'ee/app/controllers/projects/security/scanned_resources_controller.rb'
- 'ee/app/models/product_analytics/jitsu_authentication.rb'
- 'ee/app/serializers/integrations/zentao_serializers/issue_entity.rb'
- - 'ee/app/services/integrations/slack_interactions/incident_management/incident_modal_closed_service.rb'
- - 'ee/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb'
- - 'ee/app/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler.rb'
- 'ee/db/fixtures/development/35_merge_request_predictions.rb'
- 'ee/lib/api/analytics/product_analytics.rb'
- 'ee/lib/ee/gitlab/scim/attribute_transform.rb'
- 'ee/lib/elastic/latest/note_class_proxy.rb'
- 'ee/lib/gitlab/applied_ml/suggested_reviewers/client.rb'
- - 'ee/lib/slack/block_kit/app_home_opened.rb'
- 'ee/spec/controllers/admin/audit_logs_controller_spec.rb'
- 'ee/spec/controllers/groups/audit_events_controller_spec.rb'
- 'ee/spec/controllers/projects/audit_events_controller_spec.rb'
@@ -51,13 +47,9 @@ Lint/SymbolConversion:
- 'ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/create_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/destroy_spec.rb'
- 'ee/spec/requests/api/graphql/mutations/audit_events/external_audit_event_destinations/update_spec.rb'
- - 'ee/spec/requests/api/integrations/slack/interactions_spec.rb'
- - 'ee/spec/requests/api/integrations/slack/options_spec.rb'
- 'ee/spec/requests/api/scim/group_scim_spec.rb'
- 'ee/spec/requests/api/scim/instance_scim_spec.rb'
- 'ee/spec/services/elastic/data_migration_service_spec.rb'
- - 'ee/spec/services/integrations/slack_interactions/incident_management/incident_modal_closed_service_spec.rb'
- - 'ee/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb'
- 'ee/spec/services/security/token_revocation_service_spec.rb'
- 'ee/spec/support/helpers/subscription_portal_helpers.rb'
- 'ee/spec/support/prometheus/additional_metrics_shared_examples.rb'
diff --git a/.rubocop_todo/rspec/missing_feature_category.yml b/.rubocop_todo/rspec/missing_feature_category.yml
index bf9185bd243..205f633e9cb 100644
--- a/.rubocop_todo/rspec/missing_feature_category.yml
+++ b/.rubocop_todo/rspec/missing_feature_category.yml
@@ -1006,7 +1006,6 @@ RSpec/MissingFeatureCategory:
- 'ee/spec/lib/quality/seeders/vulnerabilities_spec.rb'
- 'ee/spec/lib/sidebars/groups/menus/analytics_menu_spec.rb'
- 'ee/spec/lib/sidebars/groups/menus/security_compliance_menu_spec.rb'
- - 'ee/spec/lib/slack/block_kit/app_home_opened_spec.rb'
- 'ee/spec/lib/system_check/app/search_check_spec.rb'
- 'ee/spec/mailers/ci_minutes_usage_mailer_spec.rb'
- 'ee/spec/mailers/credentials_inventory_mailer_spec.rb'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cfd59bef574..310cce472d8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,25 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 15.11.1 (2023-05-01)
+
+### Fixed (2 changes)
+
+- [Fix search cron worker when indexing is disabled](gitlab-org/security/gitlab@e543d1c8e0bb5d9e498beb51cd264c8bc6825cc0) **GitLab Enterprise Edition**
+- [Fix Web IDE Beta icons not loading in Safari](gitlab-org/security/gitlab@f11e5b37c05f314efe5a6895d385bc4ed284d217)
+
+### Security (9 changes)
+
+- [Set minimum role for importing projects to Maintainer](gitlab-org/security/gitlab@bd6bd7340736767a9dc7589ab798c75dbea607d5) ([merge request](gitlab-org/security/gitlab!3214))
+- [Commit trailers now only match public user email addresses](gitlab-org/security/gitlab@2c307a557ac7b3e32c4201b639d684fa1389351c) ([merge request](gitlab-org/security/gitlab!3207))
+- [Handle invalid URLs in asset proxy](gitlab-org/security/gitlab@2748c81f96539de154b3fb89ca2e72658bda617b) ([merge request](gitlab-org/security/gitlab!3211))
+- [Relay state to check for only allowing sub paths](gitlab-org/security/gitlab@be654790e2844dcc246e3cbf9d06280360e2a134) ([merge request](gitlab-org/security/gitlab!3218))
+- [Prohibit 40 character hex sets at beginning of path-based branch name](gitlab-org/security/gitlab@5bb78addd26b3c53750aaeeb575e1f2d46728260) ([merge request](gitlab-org/security/gitlab!3198))
+- [Add specs for external users flag](gitlab-org/security/gitlab@b45c2e236f530558cd850fa53ef08cd2ee58d79a) ([merge request](gitlab-org/security/gitlab!3206))
+- [Update policy to prevent banned members from accessing public projects](gitlab-org/security/gitlab@e8848b32fd03c0bc4b46f3fa9efb73550bacf615) ([merge request](gitlab-org/security/gitlab!3217))
+- [Use dummy filename as filename when viewing raw xml files](gitlab-org/security/gitlab@ac38e0600b5dedd616ae653a17ad838f009f25f0) ([merge request](gitlab-org/security/gitlab!3199))
+- [Authorize access to vulnerabilitiesCountByDay resolver](gitlab-org/security/gitlab@70264a8cc4e10e635ac4c1ebed15a01b1201c688) ([merge request](gitlab-org/security/gitlab!3222))
+
## 15.11.0 (2023-04-21)
### Added (175 changes)
@@ -817,6 +836,20 @@ entry.
- [Update header section](gitlab-org/gitlab@cf4ab283267d84fa1c0dc90fefb1b6ddd2617b5c) ([merge request](gitlab-org/gitlab!114102)) **GitLab Enterprise Edition**
- [Swap merge_request_user_mentions.note_id to bigint](gitlab-org/gitlab@96baed47326db4f0cc9f60b2e74215211effd814) ([merge request](gitlab-org/gitlab!113928))
+## 15.10.5 (2023-05-01)
+
+### Security (9 changes)
+
+- [Set minimum role for importing projects to Maintainer](gitlab-org/security/gitlab@d4cff7e53961d819b30ae748a38e4c8e4d856b32) ([merge request](gitlab-org/security/gitlab!3215))
+- [Commit trailers now only match public user email addresses](gitlab-org/security/gitlab@4948acdb39ba6ae9a71ef133e38ec47327d14f97) ([merge request](gitlab-org/security/gitlab!3208))
+- [Handle invalid URLs in asset proxy](gitlab-org/security/gitlab@b22e923ab3d48d9389311192d92dd89e2bfc24aa) ([merge request](gitlab-org/security/gitlab!3212))
+- [Relay state to check for only allowing sub paths](gitlab-org/security/gitlab@24f84fafd65dfedf36e859d305dd46bf3e71c8dc) ([merge request](gitlab-org/security/gitlab!3220))
+- [Prohibit 40 character hex sets at beginning of path-based branch name](gitlab-org/security/gitlab@71d30b6537f6853fef45acba16ab26b6f32718f7) ([merge request](gitlab-org/security/gitlab!3194))
+- [Add specs for external users flag](gitlab-org/security/gitlab@dfdb540285e573bd55a8647db4de8370ba6b3286) ([merge request](gitlab-org/security/gitlab!3190))
+- [Update policy to prevent banned members from accessing public projects](gitlab-org/security/gitlab@bc211b8be25e56f35c80d2331447f251c7a7dd56) ([merge request](gitlab-org/security/gitlab!3186))
+- [Use dummy filename as filename when viewing raw xml files](gitlab-org/security/gitlab@6d871f56d7a343d705f8c849d24a94b3528c3a97) ([merge request](gitlab-org/security/gitlab!3192))
+- [Authorize access to vulnerabilitiesCountByDay resolver](gitlab-org/security/gitlab@888c187aab7c7062ea43b61a282c4dea8c6a47be) ([merge request](gitlab-org/security/gitlab!3180))
+
## 15.10.4 (2023-04-21)
### Fixed (1 change)
@@ -1596,6 +1629,19 @@ entry.
- [Update submit buttons to use Pajamas component](gitlab-org/gitlab@4ffb92755e6be3268c78f02e471f5c2a21f437be) ([merge request](gitlab-org/gitlab!114246))
+## 15.9.6 (2023-05-01)
+
+### Security (8 changes)
+
+- [Resolve ambiguous references for archive metadata](gitlab-org/security/gitlab@233b0f78baf8eb9adcfd77e4d1aa606d54472d34) ([merge request](gitlab-org/security/gitlab!3203))
+- [Commit trailers now only match public user email addresses](gitlab-org/security/gitlab@e360774721bb9b5f6a2da9908ef08d92ad5a79cd) ([merge request](gitlab-org/security/gitlab!3209))
+- [Handle invalid URLs in asset proxy](gitlab-org/security/gitlab@ee6df7196b14014b5416f090a684e3b6ba600b5a) ([merge request](gitlab-org/security/gitlab!3213))
+- [Relay state to check for only allowing sub paths](gitlab-org/security/gitlab@c690eec0a2f8aa506b8ff3ffadf306aa91501648) ([merge request](gitlab-org/security/gitlab!3221))
+- [Prohibit 40 character hex sets at beginning of path-based branch name](gitlab-org/security/gitlab@889683b6b1884bfc36208dfae899d0fb9437246c) ([merge request](gitlab-org/security/gitlab!3195))
+- [Update policy to prevent banned members from accessing public projects](gitlab-org/security/gitlab@1abcbdc23881dab5f675e858afa31be87d5d47ce) ([merge request](gitlab-org/security/gitlab!3187))
+- [Use dummy filename as filename when viewing raw xml files](gitlab-org/security/gitlab@33563159bcc7d46c95f013bf089ed94128f10379) ([merge request](gitlab-org/security/gitlab!3193))
+- [Authorize access to vulnerabilitiesCountByDay resolver](gitlab-org/security/gitlab@4b0825f79b0a27eeddabaee0b3a7f627b2487706) ([merge request](gitlab-org/security/gitlab!3181))
+
## 15.9.5 (2023-04-21)
### Fixed (1 change)
diff --git a/Gemfile b/Gemfile
index 4240a679c78..914a81cc5f0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,7 +2,7 @@
source 'https://rubygems.org'
-if ENV['BUNDLER_CHECKSUM_VERIFICATION_OPT_IN'] # this verification is still experimental
+if ENV.fetch('BUNDLER_CHECKSUM_VERIFICATION_OPT_IN', 'false') != 'false' # this verification is still experimental
$LOAD_PATH.unshift(File.expand_path("vendor/gems/bundler-checksum/lib", __dir__))
require 'bundler-checksum'
BundlerChecksum.patch!
diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_details.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_details.vue
deleted file mode 100644
index f49411604f1..00000000000
--- a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_details.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
-import { uniqueId } from 'lodash';
-import { GlButton, GlCollapse } from '@gitlab/ui';
-import { getTimeago } from '~/lib/utils/datetime_utility';
-import { __, sprintf } from '~/locale';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-
-export default {
- components: {
- GlButton,
- GlCollapse,
- },
- directives: { SafeHtml },
- props: {
- report: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- isVisible: false,
- collapseId: uniqueId('abuse-report-detail-'),
- };
- },
- computed: {
- toggleText() {
- return this.isVisible ? __('Hide details') : __('Show details');
- },
- reportedUserCreatedAt() {
- const { reportedUser } = this.report;
- return sprintf(__('User joined %{timeAgo}'), {
- timeAgo: getTimeago().format(reportedUser.createdAt),
- });
- },
- },
- methods: {
- toggleCollapse() {
- this.isVisible = !this.isVisible;
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-flex-direction-column">
- <gl-collapse :id="collapseId" v-model="isVisible">
- <dl class="gl-mb-2">
- <dd>{{ reportedUserCreatedAt }}</dd>
-
- <dt>{{ __('Message') }}</dt>
- <dd v-safe-html="report.message"></dd>
- </dl>
- </gl-collapse>
- <div>
- <gl-button
- :aria-expanded="`${isVisible}`"
- :aria-controls="collapseId"
- size="small"
- variant="link"
- @click="toggleCollapse"
- >{{ toggleText }}
- </gl-button>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
index a9fe59a7b85..b8a4640de59 100644
--- a/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
+++ b/app/assets/javascripts/admin/abuse_reports/components/abuse_report_row.vue
@@ -1,20 +1,15 @@
<script>
-import { GlSprintf, GlLink } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
import { getTimeago } from '~/lib/utils/datetime_utility';
import { queryToObject } from '~/lib/utils/url_utility';
-import { __, sprintf } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { SORT_UPDATED_AT } from '../constants';
-import AbuseReportActions from './abuse_report_actions.vue';
-import AbuseReportDetails from './abuse_report_details.vue';
export default {
name: 'AbuseReportRow',
components: {
- AbuseReportDetails,
GlLink,
- GlSprintf,
- AbuseReportActions,
ListItem,
},
props: {
@@ -33,22 +28,14 @@ export default {
return sprintf(template, { timeAgo: getTimeago().format(timeAgo) });
},
- reported() {
- const { reportedUser } = this.report;
- return sprintf('%{userLinkStart}%{reported}%{userLinkEnd}', {
- reported: reportedUser.name,
- });
- },
- reporter() {
- const { reporter } = this.report;
- return sprintf('%{reporterLinkStart}%{reporter}%{reporterLinkEnd}', {
- reporter: reporter.name,
- });
- },
title() {
- const { category } = this.report;
- const template = __('%{reported} reported for %{category} by %{reporter}');
- return sprintf(template, { reported: this.reported, reporter: this.reporter, category });
+ const { reportedUser, category, reporter } = this.report;
+ const template = s__('AbuseReports|%{reportedUser} reported for %{category} by %{reporter}');
+ return sprintf(template, {
+ reportedUser: reportedUser?.name || s__('AbuseReports|Deleted user'),
+ reporter: reporter?.name || s__('AbuseReports|Deleted user'),
+ category,
+ });
},
},
};
@@ -57,25 +44,13 @@ export default {
<template>
<list-item data-testid="abuse-report-row">
<template #left-primary>
- <div class="gl-font-weight-normal gl-mb-2" data-testid="title">
- <gl-sprintf :message="title">
- <template #userLink="{ content }">
- <gl-link :href="report.reportedUserPath">{{ content }}</gl-link>
- </template>
- <template #reporterLink="{ content }">
- <gl-link :href="report.reporterPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </div>
- </template>
-
- <template #left-secondary>
- <abuse-report-details :report="report" />
+ <gl-link :href="report.reportPath" class="gl-font-weight-normal gl-mb-2" data-testid="title">
+ {{ title }}
+ </gl-link>
</template>
<template #right-secondary>
<div data-testid="abuse-report-date">{{ displayDate }}</div>
- <abuse-report-actions :report="report" />
</template>
</list-item>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
index 9cbf60b1c8f..b7616c02601 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
@@ -1,11 +1,13 @@
<script>
import { __, s__, sprintf } from '~/locale';
+import Tracking from '~/tracking';
import {
COMMIT_ACTION_CREATE,
COMMIT_ACTION_UPDATE,
COMMIT_FAILURE,
COMMIT_SUCCESS,
COMMIT_SUCCESS_WITH_REDIRECT,
+ pipelineEditorTrackingOptions,
} from '../../constants';
import commitCIFile from '../../graphql/mutations/commit_ci_file.mutation.graphql';
import updateCurrentBranchMutation from '../../graphql/mutations/client/update_current_branch.mutation.graphql';
@@ -26,6 +28,7 @@ export default {
components: {
CommitForm,
},
+ mixins: [Tracking.mixin()],
inject: ['projectFullPath', 'ciConfigPath'],
props: {
ciFileContent: {
@@ -78,6 +81,8 @@ export default {
async onCommitSubmit({ message, sourceBranch, openMergeRequest }) {
this.isSaving = true;
+ this.trackCommitEvent();
+
try {
const {
data: {
@@ -131,6 +136,10 @@ export default {
this.isSaving = false;
}
},
+ trackCommitEvent() {
+ const { label, actions } = pipelineEditorTrackingOptions;
+ this.track(actions.commitCiConfig, { label, property: this.action });
+ },
updateCurrentBranch(currentBranch) {
this.$apollo.mutate({
mutation: updateCurrentBranchMutation,
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
index 42e2d34fa3a..9179fe9d075 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
@@ -6,7 +6,7 @@ import SourceEditor from '~/vue_shared/components/source_editor.vue';
export default {
i18n: {
- viewOnlyMessage: s__('Pipelines|Merged YAML is view only'),
+ viewOnlyMessage: s__('Pipelines|Full configuration is view only'),
},
components: {
SourceEditor,
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index fd6547468d9..e965ac12aa5 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -31,7 +31,7 @@ export default {
tabEdit: s__('Pipelines|Edit'),
tabGraph: s__('Pipelines|Visualize'),
tabLint: s__('Pipelines|Lint'),
- tabMergedYaml: s__('Pipelines|View merged YAML'),
+ tabMergedYaml: s__('Pipelines|Full configuration'),
tabValidate: s__('Pipelines|Validate'),
empty: {
visualization: s__(
@@ -41,12 +41,12 @@ export default {
'PipelineEditor|The CI/CD configuration is continuously validated. Errors and warnings are displayed when the CI/CD configuration file is not empty.',
),
merge: s__(
- 'PipelineEditor|The merged YAML view is displayed when the CI/CD configuration file has valid syntax.',
+ 'PipelineEditor|The full configuration view is displayed when the CI/CD configuration file has valid syntax.',
),
},
},
errorTexts: {
- loadMergedYaml: s__('Pipelines|Could not load merged YAML content'),
+ loadMergedYaml: s__('Pipelines|Could not load full configuration content'),
},
query: {
TAB_QUERY_PARAM,
diff --git a/app/assets/javascripts/ci/pipeline_editor/constants.js b/app/assets/javascripts/ci/pipeline_editor/constants.js
index e775dc5147a..912e0fcbff9 100644
--- a/app/assets/javascripts/ci/pipeline_editor/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/constants.js
@@ -67,6 +67,7 @@ export const pipelineEditorTrackingOptions = {
actions: {
browseTemplates: 'browse_templates',
closeHelpDrawer: 'close_help_drawer',
+ commitCiConfig: 'commit_ci_config',
helpDrawerLinks: {
[CI_EXAMPLES_LINK]: 'visit_help_drawer_link_ci_examples',
[CI_HELP_LINK]: 'visit_help_drawer_link_ci_help',
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index fadce6457bc..6efb7a6cdf1 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -59,11 +59,20 @@ export default {
item() {
return { text: this.displayText };
},
+ isButtonTrigger() {
+ return this.triggerElement === TRIGGER_ELEMENT_BUTTON;
+ },
+ isWithEmojiTrigger() {
+ return this.triggerElement === TRIGGER_ELEMENT_WITH_EMOJI;
+ },
+ isDropdownWithEmojiTrigger() {
+ return this.triggerElement === TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI;
+ },
+ isDisclosureTrigger() {
+ return this.triggerElement === TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN;
+ },
},
methods: {
- checkTrigger(targetTriggerElement) {
- return this.triggerElement === targetTriggerElement;
- },
openModal() {
eventHub.$emit('openModal', { source: this.triggerSource });
},
@@ -72,16 +81,12 @@ export default {
this.$emit('modal-opened');
},
},
- TRIGGER_ELEMENT_BUTTON,
- TRIGGER_ELEMENT_WITH_EMOJI,
- TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI,
- TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN,
};
</script>
<template>
<gl-button
- v-if="checkTrigger($options.TRIGGER_ELEMENT_BUTTON)"
+ v-if="isButtonTrigger"
v-bind="componentAttributes"
:variant="variant"
:icon="icon"
@@ -89,16 +94,12 @@ export default {
>
{{ displayText }}
</gl-button>
- <gl-link
- v-else-if="checkTrigger($options.TRIGGER_ELEMENT_WITH_EMOJI)"
- v-bind="componentAttributes"
- @click="openModal"
- >
+ <gl-link v-else-if="isWithEmojiTrigger" v-bind="componentAttributes" @click="openModal">
{{ displayText }}
<gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" :data-name="icon" />
</gl-link>
<gl-dropdown-item
- v-else-if="checkTrigger($options.TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI)"
+ v-else-if="isDropdownWithEmojiTrigger"
v-bind="componentAttributes"
button-class="top-nav-menu-item"
@click="openModal"
@@ -107,7 +108,7 @@ export default {
<gl-emoji class="gl-vertical-align-baseline gl-reset-font-size gl-mr-1" :data-name="icon" />
</gl-dropdown-item>
<gl-disclosure-dropdown-item
- v-else-if="checkTrigger($options.TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN)"
+ v-else-if="isDisclosureTrigger"
v-bind="componentAttributes"
:item="item"
@action="handleDisclosureDropdownAction"
diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
index ced99de28bc..f95db498c4c 100644
--- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
+++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue
@@ -133,6 +133,8 @@ export default {
fetchJobsByStatus(scope) {
this.infiniteScrollingTriggered = false;
+ if (this.scope === scope) return;
+
this.scope = scope;
this.$apollo.queries.jobs.refetch({ statuses: scope });
diff --git a/app/assets/javascripts/labels/label_manager.js b/app/assets/javascripts/labels/label_manager.js
index f4d7c610cae..e3d56df53f8 100644
--- a/app/assets/javascripts/labels/label_manager.js
+++ b/app/assets/javascripts/labels/label_manager.js
@@ -106,7 +106,7 @@ export default class LabelManager {
if (action === 'remove') {
$('.js-priority-badge', $label).remove();
} else {
- $('.label-links', $label).append(this.$badgeItemTemplate.clone().html());
+ $('.label-links', $label).prepend(this.$badgeItemTemplate.clone().html());
}
}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 8fc341c4ca8..237e8c68be4 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -252,10 +252,11 @@ export default class MergeRequestTabs {
}
recallScroll(action) {
const storedPosition = this.scrollPositions[action];
+ if (storedPosition == null) return;
setTimeout(() => {
window.scrollTo({
- top: storedPosition && storedPosition > 0 ? storedPosition : 0,
+ top: storedPosition > 0 ? storedPosition : 0,
left: 0,
behavior: 'auto',
});
@@ -308,7 +309,7 @@ export default class MergeRequestTabs {
const tab = this.mergeRequestTabs.querySelector(`.${action}-tab`);
if (tab) tab.classList.add('active');
- if (!this.loadedPages[action] && action in pageBundles) {
+ if (isInVueNoteablePage() && !this.loadedPages[action] && action in pageBundles) {
toggleLoader(true);
pageBundles[action]()
.then(({ default: init }) => {
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
index e56ec5375c2..38301ce1d8a 100644
--- a/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
+++ b/app/assets/javascripts/pages/admin/jobs/components/table/admin_jobs_table_app.vue
@@ -153,6 +153,8 @@ export default {
fetchJobsByStatus(scope) {
this.infiniteScrollingTriggered = false;
+ if (this.scope === scope) return;
+
this.scope = scope;
this.$apollo.queries.jobs.refetch({ statuses: scope });
diff --git a/app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue b/app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue
new file mode 100644
index 00000000000..cbb80a5175f
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/jobs/components/table/cell/project_cell.vue
@@ -0,0 +1,28 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlLink,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ projectName() {
+ return this.job.pipeline?.project?.fullPath;
+ },
+ projectUrl() {
+ return this.job.pipeline?.project?.webUrl;
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-text-truncate">
+ <gl-link :href="projectUrl"> {{ projectName }}</gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/token_access/components/outbound_token_access.vue b/app/assets/javascripts/token_access/components/outbound_token_access.vue
index 7a1b1ed6586..9c9b0d37b68 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -12,6 +12,7 @@ import {
import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import addProjectCIJobTokenScopeMutation from '../graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '../graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
import updateCIJobTokenScopeMutation from '../graphql/mutations/update_ci_job_token_scope.mutation.graphql';
@@ -19,6 +20,8 @@ import getCIJobTokenScopeQuery from '../graphql/queries/get_ci_job_token_scope.q
import getProjectsWithCIJobTokenScopeQuery from '../graphql/queries/get_projects_with_ci_job_token_scope.query.graphql';
import TokenProjectsTable from './token_projects_table.vue';
+// Note: This component will be removed in 17.0, as the outbound access token is getting deprecated
+// Some warnings are behind the `frozen_outbound_job_token_scopes` feature flag
export default {
i18n: {
toggleLabelTitle: s__('CICD|Limit CI_JOB_TOKEN access'),
@@ -34,7 +37,14 @@ export default {
addProjectPlaceholder: __('Paste project path (i.e. gitlab-org/gitlab)'),
projectsFetchError: __('There was a problem fetching the projects'),
scopeFetchError: __('There was a problem fetching the job token scope value'),
+ outboundTokenAlertDeprecationMessage: s__(
+ `CICD|The %{boldStart}Limit CI_JOB_TOKEN%{boldEnd} scope is deprecated and will be removed the 17.0 milestone. Configure the %{boldStart}CI_JOB_TOKEN%{boldEnd} allowlist instead. %{linkStart}How do I do this?%{linkEnd}`,
+ ),
+ disableToggleWarning: s__('CICD|Disabling this feature is a permanent change.'),
},
+ deprecationDocumentationLink: helpPagePath('ci/jobs/ci_job_token', {
+ anchor: 'limit-your-projects-job-token-access',
+ }),
fields: [
{
key: 'project',
@@ -67,6 +77,7 @@ export default {
GlToggle,
TokenProjectsTable,
},
+ mixins: [glFeatureFlagMixin()],
inject: {
fullPath: {
default: '',
@@ -116,6 +127,15 @@ export default {
ciJobTokenHelpPage() {
return helpPagePath('ci/jobs/ci_job_token#limit-your-projects-job-token-access');
},
+ disableOutboundToken() {
+ return (
+ this.glFeatures?.frozenOutboundJobTokenScopes &&
+ !this.glFeatures?.frozenOutboundJobTokenScopesOverride
+ );
+ },
+ disableTokenToggle() {
+ return !this.jobTokenScopeEnabled && this.disableOutboundToken;
+ },
},
methods: {
async updateCIJobTokenScope() {
@@ -205,9 +225,33 @@ export default {
<div>
<gl-loading-icon v-if="$apollo.loading" size="lg" class="gl-mt-5" />
<template v-else>
+ <gl-alert
+ v-if="disableOutboundToken"
+ class="gl-mb-3"
+ variant="warning"
+ :dismissible="false"
+ :show-icon="false"
+ data-testid="deprecation-alert"
+ >
+ <gl-sprintf :message="$options.i18n.outboundTokenAlertDeprecationMessage">
+ <template #bold="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.deprecationDocumentationLink"
+ class="inline-link"
+ target="_blank"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<gl-toggle
v-model="jobTokenScopeEnabled"
:label="$options.i18n.toggleLabelTitle"
+ :disabled="disableTokenToggle"
@change="updateCIJobTokenScope"
>
<template #help>
@@ -216,6 +260,7 @@ export default {
<gl-link :href="ciJobTokenHelpPage" class="inline-link" target="_blank">
{{ content }}
</gl-link>
+ <strong v-if="disableOutboundToken">{{ $options.i18n.disableToggleWarning }} </strong>
</template>
</gl-sprintf>
</template>
@@ -229,7 +274,9 @@ export default {
<template #default>
<gl-form-input
v-model="targetProjectPath"
+ :disabled="disableOutboundToken"
:placeholder="$options.i18n.addProjectPlaceholder"
+ data-testid="project-path-input"
/>
</template>
<template #footer>
@@ -240,7 +287,7 @@ export default {
</template>
</gl-card>
<gl-alert
- v-if="!jobTokenScopeEnabled"
+ v-if="!jobTokenScopeEnabled && !disableOutboundToken"
class="gl-mb-3"
variant="warning"
:dismissible="false"
diff --git a/app/assets/javascripts/token_access/components/token_access_app.vue b/app/assets/javascripts/token_access/components/token_access_app.vue
index 089159ac87b..beaa564db51 100644
--- a/app/assets/javascripts/token_access/components/token_access_app.vue
+++ b/app/assets/javascripts/token_access/components/token_access_app.vue
@@ -4,6 +4,7 @@ import InboundTokenAccess from './inbound_token_access.vue';
import OptInJwt from './opt_in_jwt.vue';
export default {
+ name: 'TokenAccessApp',
components: {
OutboundTokenAccess,
InboundTokenAccess,
diff --git a/app/assets/javascripts/token_access/index.js b/app/assets/javascripts/token_access/index.js
index 0253abe393e..9258d5eba45 100644
--- a/app/assets/javascripts/token_access/index.js
+++ b/app/assets/javascripts/token_access/index.js
@@ -20,6 +20,7 @@ export const initTokenAccess = (containerId = 'js-ci-token-access-app') => {
return new Vue({
el: containerEl,
+ name: 'TokenAccessAppsRoot',
apolloProvider,
provide: {
fullPath,
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 472e08bb427..15d4a0fec9a 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -85,13 +85,19 @@
padding: 0;
margin-bottom: 0;
- > li:not(.empty-message):not(.no-border) {
- background-color: $white;
+ > li:not(.empty-message):not(.no-border) .label-content {
display: flex;
justify-content: space-between;
.prioritized-labels:not(.is-not-draggable) & {
cursor: grab;
+ border: 1px solid transparent;
+
+ &:hover,
+ &:focus-within {
+ background-color: $white;
+ border-color: $gray-50;
+ }
&:active {
cursor: grabbing;
@@ -105,6 +111,10 @@
}
}
+.label-list-item:not(:last-of-type) {
+ border-bottom: 1px solid $border-color;
+}
+
.prioritized-labels .add-priority,
.other-labels .remove-priority {
display: none;
@@ -133,7 +143,7 @@
font-size: $label-font-size;
}
-.label-list-item {
+.label-content {
.label-name {
width: 200px;
@@ -158,7 +168,7 @@
@media (max-width: map-get($grid-breakpoints, md)-1) {
.manage-labels-list {
- > li:not(.empty-message):not(.no-border) {
+ > li:not(.empty-message):not(.no-border) .label-content {
flex-wrap: wrap;
}
diff --git a/app/controllers/concerns/search_rate_limitable.rb b/app/controllers/concerns/search_rate_limitable.rb
index a77ebd276b6..7cce30dbb3c 100644
--- a/app/controllers/concerns/search_rate_limitable.rb
+++ b/app/controllers/concerns/search_rate_limitable.rb
@@ -7,9 +7,25 @@ module SearchRateLimitable
def check_search_rate_limit!
if current_user
- check_rate_limit!(:search_rate_limit, scope: [current_user])
+ # Because every search in the UI typically runs concurrent searches with different
+ # scopes to get counts, we apply rate limits on the search scope if it is present.
+ #
+ # If abusive search is detected, we have stricter limits and ignore the search scope.
+ check_rate_limit!(:search_rate_limit, scope: [current_user, safe_search_scope].compact)
else
check_rate_limit!(:search_rate_limit_unauthenticated, scope: [request.ip])
end
end
+
+ def safe_search_scope
+ # Sometimes search scope can have abusive length or invalid keyword. We don't want
+ # to send those to redis for rate limit checks, so we guard against that here.
+ return if Feature.disabled?(:search_rate_limited_scopes) || abuse_detected?
+
+ params[:scope]
+ end
+
+ def abuse_detected?
+ Gitlab::Search::Params.new(params, detect_abuse: true).abusive?
+ end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 626587deb71..ce760051f79 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -16,6 +16,8 @@ module Projects
push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:ci_limit_environment_scope, @project)
push_frontend_feature_flag(:create_runner_workflow_for_namespace, @project.namespace)
+ push_frontend_feature_flag(:frozen_outbound_job_token_scopes, @project)
+ push_frontend_feature_flag(:frozen_outbound_job_token_scopes_override, @project)
end
helper_method :highlight_badge
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 0fac2cb5fc5..57075a44d0f 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -116,7 +116,7 @@ module AvatarsHelper
private
def avatar_icon_by_user_email_or_gravatar(email, size, scale, only_path:)
- user = User.find_by_any_email(email)
+ user = User.with_public_email(email).first
if user
avatar_icon_for_user(user, size, scale, only_path: only_path)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2274c72872b..c3121851700 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1483,6 +1483,7 @@ class MergeRequest < ApplicationRecord
def fetch_ref!
target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
+ expire_ancestor_cache
end
# Returns the current merge-ref HEAD commit.
@@ -2042,6 +2043,10 @@ class MergeRequest < ApplicationRecord
self.draft = draft?
end
+ def expire_ancestor_cache
+ project.repository.expire_ancestor_cache(target_branch_sha, diff_head_sha)
+ end
+
def missing_report_error(report_type)
{ status: :error, status_reason: "This merge request does not have #{report_type} reports" }
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 9b1e7423efa..e942157993b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1004,7 +1004,7 @@ class Repository
def ancestor?(ancestor_id, descendant_id)
return false if ancestor_id.nil? || descendant_id.nil?
- cache_key = "ancestor:#{ancestor_id}:#{descendant_id}"
+ cache_key = ancestor_cache_key(ancestor_id, descendant_id)
request_store_cache.fetch(cache_key) do
cache.fetch(cache_key) do
raw_repository.ancestor?(ancestor_id, descendant_id)
@@ -1012,6 +1012,12 @@ class Repository
end
end
+ def expire_ancestor_cache(ancestor_id, descendant_id)
+ cache_key = ancestor_cache_key(ancestor_id, descendant_id)
+ request_store_cache.expire(cache_key)
+ cache.expire(cache_key)
+ end
+
def clone_as_mirror(url, http_authorization_header: "", resolved_address: "")
import_repository(url, http_authorization_header: http_authorization_header, mirror: true, resolved_address: resolved_address)
end
@@ -1232,6 +1238,10 @@ class Repository
private
+ def ancestor_cache_key(ancestor_id, descendant_id)
+ "ancestor:#{ancestor_id}:#{descendant_id}"
+ end
+
# TODO Genericize finder, later split this on finders by Ref or Oid
# https://gitlab.com/gitlab-org/gitlab/issues/19877
def find_commit(oid_or_ref)
diff --git a/app/serializers/admin/abuse_report_entity.rb b/app/serializers/admin/abuse_report_entity.rb
index 456640bf836..58637445e81 100644
--- a/app/serializers/admin/abuse_report_entity.rb
+++ b/app/serializers/admin/abuse_report_entity.rb
@@ -3,50 +3,21 @@
module Admin
class AbuseReportEntity < Grape::Entity
include RequestAwareEntity
- include MarkupHelper
expose :category
expose :created_at
expose :updated_at
expose :reported_user do |report|
- UserEntity.represent(report.user, only: [:name, :created_at])
+ UserEntity.represent(report.user, only: [:name])
end
expose :reporter do |report|
UserEntity.represent(report.reporter, only: [:name])
end
- expose :reported_user_path do |report|
- user_path(report.user)
- end
-
- expose :reporter_path do |report|
- user_path(report.reporter)
- end
-
- expose :user_blocked do |report|
- report.user.blocked?
- end
-
- expose :block_user_path do |report|
- block_admin_user_path(report.user)
- end
-
- expose :remove_report_path do |report|
- admin_abuse_report_path(report)
- end
-
expose :report_path do |report|
admin_abuse_report_path(report)
end
-
- expose :remove_user_and_report_path do |report|
- admin_abuse_report_path(report, remove_user: true)
- end
-
- expose :message do |report|
- markdown_field(report, :message)
- end
end
end
diff --git a/app/services/integrations/slack_event_service.rb b/app/services/integrations/slack_event_service.rb
new file mode 100644
index 00000000000..65f3c226e34
--- /dev/null
+++ b/app/services/integrations/slack_event_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+# Performs the initial handling of event payloads sent from Slack to GitLab.
+# See `API::Integrations::Slack::Events` which calls this service.
+module Integrations
+ class SlackEventService
+ URL_VERIFICATION_EVENT = 'url_verification'
+
+ UnknownEventError = Class.new(StandardError)
+
+ def initialize(params)
+ # When receiving URL verification events, params[:type] is 'url_verification'.
+ # For all other events we subscribe to, params[:type] is 'event_callback' and
+ # the specific type of the event will be in params[:event][:type].
+ # Remove both of these from the params before they are passed to the services.
+ type = params.delete(:type)
+ type = params[:event].delete(:type) if type == 'event_callback'
+
+ @slack_event = type
+ @params = params
+ end
+
+ def execute
+ raise UnknownEventError, "Unable to handle event type: '#{slack_event}'" unless routable_event?
+
+ payload = route_event
+
+ ServiceResponse.success(payload: payload)
+ end
+
+ private
+
+ # The `url_verification` slack_event response must be returned to Slack in-request,
+ # so for this event we call the service directly instead of through a worker.
+ #
+ # All other events must be handled asynchronously in order to return a 2xx response
+ # immediately to Slack in the request. See https://api.slack.com/apis/connections/events-api.
+ def route_in_request?
+ slack_event == URL_VERIFICATION_EVENT
+ end
+
+ def routable_event?
+ route_in_request? || route_to_event_worker?
+ end
+
+ def route_to_event_worker?
+ SlackEventWorker.event?(slack_event)
+ end
+
+ # Returns a payload for the service response.
+ def route_event
+ return SlackEvents::UrlVerificationService.new(params).execute if route_in_request?
+
+ SlackEventWorker.perform_async(slack_event: slack_event, params: params)
+
+ {}
+ end
+
+ attr_reader :slack_event, :params
+ end
+end
diff --git a/app/services/integrations/slack_events/app_home_opened_service.rb b/app/services/integrations/slack_events/app_home_opened_service.rb
new file mode 100644
index 00000000000..48dda324270
--- /dev/null
+++ b/app/services/integrations/slack_events/app_home_opened_service.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+# Handles the Slack `app_home_opened` event sent from Slack to GitLab.
+# Responds with a POST to the Slack API 'views.publish' method.
+#
+# See:
+# - https://api.slack.com/methods/views.publish
+# - https://api.slack.com/events/app_home_opened
+module Integrations
+ module SlackEvents
+ class AppHomeOpenedService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(params)
+ @slack_user_id = params.dig(:event, :user)
+ @slack_workspace_id = params[:team_id]
+ end
+
+ def execute
+ # Legacy Slack App integrations will not yet have a token we can use
+ # to call the Slack API. Do nothing, and consider the service successful.
+ unless slack_installation
+ logger.info(
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id,
+ message: 'SlackInstallation record has no bot token'
+ )
+
+ return ServiceResponse.success
+ end
+
+ begin
+ response = ::Slack::API.new(slack_installation).post(
+ 'views.publish',
+ payload
+ )
+ rescue *Gitlab::HTTP::HTTP_ERRORS => e
+ return ServiceResponse
+ .error(message: 'HTTP exception when calling Slack API')
+ .track_exception(
+ as: e.class,
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id
+ )
+ end
+
+ return ServiceResponse.success if response['ok']
+
+ # For a list of errors, see:
+ # https://api.slack.com/methods/views.publish#errors
+ ServiceResponse.error(
+ message: 'Slack API returned an error',
+ payload: response
+ ).track_exception(
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id,
+ response: response.to_h
+ )
+ end
+
+ private
+
+ def slack_installation
+ SlackIntegration.with_bot.find_by_team_id(slack_workspace_id)
+ end
+ strong_memoize_attr :slack_installation
+
+ def slack_gitlab_user_connection
+ ChatNames::FindUserService.new(slack_workspace_id, slack_user_id).execute
+ end
+ strong_memoize_attr :slack_gitlab_user_connection
+
+ def payload
+ {
+ user_id: slack_user_id,
+ view: ::Slack::BlockKit::AppHomeOpened.new(
+ slack_user_id,
+ slack_workspace_id,
+ slack_gitlab_user_connection,
+ slack_installation
+ ).build
+ }
+ end
+
+ def logger
+ Gitlab::IntegrationsLogger
+ end
+
+ attr_reader :slack_user_id, :slack_workspace_id
+ end
+ end
+end
diff --git a/app/services/integrations/slack_events/url_verification_service.rb b/app/services/integrations/slack_events/url_verification_service.rb
new file mode 100644
index 00000000000..dbe2ffc77f8
--- /dev/null
+++ b/app/services/integrations/slack_events/url_verification_service.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# Returns the special URL verification response expected by Slack when the
+# GitLab Slack app is first configured to receive Slack events.
+#
+# Slack will issue the challenge request to the endpoint that receives events
+# and expect it to respond with same the `challenge` param back.
+#
+# See https://api.slack.com/apis/connections/events-api.
+module Integrations
+ module SlackEvents
+ class UrlVerificationService
+ def initialize(params)
+ @challenge = params[:challenge]
+ end
+
+ def execute
+ { challenge: challenge }
+ end
+
+ private
+
+ attr_reader :challenge
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interaction_service.rb b/app/services/integrations/slack_interaction_service.rb
new file mode 100644
index 00000000000..30e1a396f0d
--- /dev/null
+++ b/app/services/integrations/slack_interaction_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Integrations
+ class SlackInteractionService
+ UnknownInteractionError = Class.new(StandardError)
+
+ INTERACTIONS = {
+ 'view_closed' => SlackInteractions::IncidentManagement::IncidentModalClosedService,
+ 'view_submission' => SlackInteractions::IncidentManagement::IncidentModalSubmitService,
+ 'block_actions' => SlackInteractions::BlockActionService
+ }.freeze
+
+ def initialize(params)
+ @interaction_type = params.delete(:type)
+ @params = params
+ end
+
+ def execute
+ raise UnknownInteractionError, "Unable to handle interaction type: '#{interaction_type}'" \
+ unless interaction?(interaction_type)
+
+ service_class = INTERACTIONS[interaction_type]
+ service_class.new(params).execute
+
+ ServiceResponse.success
+ end
+
+ private
+
+ attr_reader :interaction_type, :params
+
+ def interaction?(type)
+ INTERACTIONS.key?(type)
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interactions/block_action_service.rb b/app/services/integrations/slack_interactions/block_action_service.rb
new file mode 100644
index 00000000000..d135635fda4
--- /dev/null
+++ b/app/services/integrations/slack_interactions/block_action_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackInteractions
+ class BlockActionService
+ ALLOWED_UPDATES_HANDLERS = {
+ 'incident_management_project' => SlackInteractions::SlackBlockActions::IncidentManagement::ProjectUpdateHandler
+ }.freeze
+
+ def initialize(params)
+ @params = params
+ end
+
+ def execute
+ actions.each do |action|
+ action_id = action[:action_id]
+
+ action_handler_class = ALLOWED_UPDATES_HANDLERS[action_id]
+ action_handler_class.new(params, action).execute
+ end
+ end
+
+ private
+
+ def actions
+ params[:actions].select { |action| ALLOWED_UPDATES_HANDLERS[action[:action_id]] }
+ end
+
+ attr_accessor :params
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interactions/incident_management/incident_modal_closed_service.rb b/app/services/integrations/slack_interactions/incident_management/incident_modal_closed_service.rb
new file mode 100644
index 00000000000..9daa5d76df7
--- /dev/null
+++ b/app/services/integrations/slack_interactions/incident_management/incident_modal_closed_service.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackInteractions
+ module IncidentManagement
+ class IncidentModalClosedService
+ def initialize(params)
+ @params = params
+ end
+
+ def execute
+ begin
+ response = close_modal
+ rescue *Gitlab::HTTP::HTTP_ERRORS => e
+ return ServiceResponse
+ .error(message: 'HTTP exception when calling Slack API')
+ .track_exception(
+ params: params,
+ as: e.class
+ )
+ end
+
+ return ServiceResponse.success if response['ok']
+
+ ServiceResponse.error(
+ message: _('Something went wrong while closing the incident form.'),
+ payload: response
+ ).track_exception(
+ response: response.to_h,
+ params: params
+ )
+ end
+
+ private
+
+ attr_accessor :params
+
+ def close_modal
+ request_body = Gitlab::Json.dump(close_request_body)
+ response_url = params.dig(:view, :private_metadata)
+
+ Gitlab::HTTP.post(response_url, body: request_body, headers: headers)
+ end
+
+ def close_request_body
+ {
+ replace_original: 'true',
+ text: _('Incident creation cancelled.')
+ }
+ end
+
+ def headers
+ { 'Content-Type' => 'application/json' }
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb b/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb
new file mode 100644
index 00000000000..34af03640d3
--- /dev/null
+++ b/app/services/integrations/slack_interactions/incident_management/incident_modal_submit_service.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackInteractions
+ module IncidentManagement
+ class IncidentModalSubmitService
+ include GitlabRoutingHelper
+ include Gitlab::Routing
+
+ IssueCreateError = Class.new(StandardError)
+
+ def initialize(params)
+ @params = params
+ @values = params.dig(:view, :state, :values)
+ @team_id = params.dig(:team, :id)
+ @user_id = params.dig(:user, :id)
+ @additional_message = ''
+ end
+
+ def execute
+ create_response = Issues::CreateService.new(
+ container: project,
+ current_user: find_user.user,
+ params: incident_params,
+ spam_params: nil
+ ).execute
+
+ raise IssueCreateError, create_response.errors.to_sentence if create_response.error?
+
+ incident = create_response.payload[:issue]
+ incident_link = incident_link_text(incident)
+ response = send_to_slack(incident_link)
+
+ return ServiceResponse.success(payload: { incident: incident }) if response['ok']
+
+ ServiceResponse.error(
+ message: _('Something went wrong when sending the incident link to Slack.'),
+ payload: response
+ ).track_exception(
+ response: response.to_h,
+ slack_workspace_id: team_id,
+ slack_user_id: user_id
+ )
+ rescue StandardError => e
+ send_to_slack(_('There was a problem creating the incident. Please try again.'))
+
+ ServiceResponse
+ .error(
+ message: e.message
+ ).track_exception(
+ slack_workspace_id: team_id,
+ slack_user_id: user_id,
+ as: e.class
+ )
+ end
+
+ private
+
+ attr_accessor :params, :values, :team_id, :user_id, :additional_message
+
+ def incident_params
+ {
+ title: values.dig(:title_input, :title, :value),
+ severity: severity,
+ confidential: confidential?,
+ description: description,
+ escalation_status: { status: status },
+ issue_type: "incident",
+ assignee_ids: [assignee],
+ label_ids: labels
+ }
+ end
+
+ def strip_markup(string)
+ SlackMarkdownSanitizer.sanitize(string)
+ end
+
+ def send_to_slack(text)
+ response_url = params.dig(:view, :private_metadata)
+
+ body = {
+ replace_original: 'true',
+ text: text
+ }
+
+ Gitlab::HTTP.post(
+ response_url,
+ body: Gitlab::Json.dump(body),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ def incident_link_text(incident)
+ "#{_('New incident has been created')}: " \
+ "<#{issue_url(incident)}|#{incident.to_reference} " \
+ "- #{strip_markup(incident.title)}>. #{@additional_message}"
+ end
+
+ def project
+ project_id = values.dig(
+ :project_and_severity_selector,
+ :incident_management_project,
+ :selected_option,
+ :value)
+
+ Project.find(project_id)
+ end
+
+ def find_user
+ ChatNames::FindUserService.new(team_id, user_id).execute
+ end
+
+ def description
+ description =
+ values.dig(:incident_description, :description, :value) ||
+ values.dig(project.id.to_s.to_sym, :description, :value)
+
+ zoom_link = values.dig(:zoom, :link, :value)
+
+ return description if zoom_link.blank?
+
+ "#{description} \n/zoom #{zoom_link}"
+ end
+
+ def confidential?
+ values.dig(:confidentiality, :confidential, :selected_options).present?
+ end
+
+ def severity
+ values.dig(:project_and_severity_selector, :severity, :selected_option, :value) || 'unknown'
+ end
+
+ def status
+ values.dig(:status_and_assignee_selector, :status, :selected_option, :value)
+ end
+
+ def assignee
+ assignee_id = values.dig(:status_and_assignee_selector, :assignee, :selected_option, :value)
+
+ return unless assignee_id
+
+ user = User.find_by_id(assignee_id)
+ member = project.member(user)
+
+ unless member
+ @additional_message =
+ "However, " \
+ "#{user.name} was not assigned to the incident as they are not a member in #{project.name}."
+
+ return
+ end
+
+ member.user_id
+ end
+
+ def labels
+ values.dig(:label_selector, :labels, :selected_options)&.pluck(:value)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler.rb b/app/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler.rb
new file mode 100644
index 00000000000..5f24c8ec4f5
--- /dev/null
+++ b/app/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+
+module Integrations
+ module SlackInteractions
+ module SlackBlockActions
+ module IncidentManagement
+ class ProjectUpdateHandler
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(params, action)
+ @view = params[:view]
+ @action = action
+ @team_id = params.dig(:view, :team_id)
+ @user_id = params.dig(:user, :id)
+ end
+
+ def execute
+ return if project_unchanged?
+ return unless allowed?
+
+ post_updated_modal
+ end
+
+ private
+
+ def allowed?
+ return false unless current_user
+
+ current_user.can?(:read_project, old_project) &&
+ current_user.can?(:read_project, new_project)
+ end
+
+ def current_user
+ ChatNames::FindUserService.new(team_id, user_id).execute&.user
+ end
+ strong_memoize_attr :current_user
+
+ def slack_installation
+ SlackIntegration.with_bot.find_by_team_id(team_id)
+ end
+ strong_memoize_attr :slack_installation
+
+ def post_updated_modal
+ modal = update_modal
+
+ begin
+ response = ::Slack::API.new(slack_installation).post(
+ 'views.update',
+ {
+ view_id: view[:id],
+ view: modal
+ }
+ )
+ rescue *::Gitlab::HTTP::HTTP_ERRORS => e
+ return ServiceResponse
+ .error(message: 'HTTP exception when calling Slack API')
+ .track_exception(
+ as: e.class,
+ slack_workspace_id: view[:team_id]
+ )
+ end
+
+ return ServiceResponse.success(message: _('Modal updated')) if response['ok']
+
+ ServiceResponse.error(
+ message: _('Something went wrong while updating the modal.'),
+ payload: response
+ ).track_exception(
+ response: response.to_h,
+ slack_workspace_id: view[:team_id],
+ slack_user_id: slack_installation.user_id
+ )
+ end
+
+ def update_modal
+ updated_view = update_incident_template
+ cleanup(updated_view)
+ end
+
+ def update_incident_template
+ updated_view = view.dup
+
+ incident_description_blocks = updated_view[:blocks].select do |block|
+ block[:block_id] == 'incident_description' || block[:block_id] == old_project.id.to_s
+ end
+
+ incident_description_blocks.first[:element][:initial_value] = read_template_content
+ incident_description_blocks.first[:block_id] = new_project.id.to_s
+
+ Integrations::SlackInteractions::IncidentManagement::IncidentModalOpenedService
+ .cache_write(view[:id], new_project.id.to_s)
+
+ updated_view
+ end
+
+ def new_project
+ Project.find(action.dig(:selected_option, :value))
+ end
+ strong_memoize_attr :new_project
+
+ def old_project
+ old_project_id = Integrations::SlackInteractions::IncidentManagement::IncidentModalOpenedService
+ .cache_read(view[:id])
+
+ Project.find(old_project_id) if old_project_id
+ end
+ strong_memoize_attr :old_project
+
+ def project_unchanged?
+ old_project == new_project
+ end
+
+ def read_template_content
+ new_project.incident_management_setting&.issue_template_content.to_s
+ end
+
+ def cleanup(view)
+ view.except!(
+ :id, :team_id, :state,
+ :hash, :previous_view_id,
+ :root_view_id, :app_id,
+ :app_installed_team_id,
+ :bot_id)
+ end
+
+ attr_accessor :view, :action, :team_id, :user_id
+ end
+ end
+ end
+ end
+end
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index a03c406acc6..80da61847ef 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -12,9 +12,11 @@
- if @labels.any?
.text-muted.gl-mb-5
= labels_function_introduction
- .other-labels
- %h4= _('Labels')
- %ul.manage-labels-list.js-other-labels
+ .other-labels.gl-rounded-base.gl-border.gl-bg-gray-10
+ .gl-px-5.gl-py-4.gl-bg-white.gl-rounded-base.gl-border-b{ class: 'gl-rounded-bottom-left-none! gl-rounded-bottom-right-none!' }
+ %h3.card-title.h5.gl-m-0.gl-relative.gl-line-height-24
+ = _('Labels')
+ %ul.manage-labels-list.js-other-labels.gl-px-3.gl-rounded-base
= render partial: 'shared/label', collection: @labels, as: :label, locals: { use_label_priority: false, subject: @group }
= paginate @labels, theme: 'gitlab'
- elsif search.present?
@@ -27,5 +29,5 @@
= render 'shared/empty_states/labels'
%template#js-badge-item-template
- %li.js-priority-badge.inline.gl-ml-3
- .label-badge.gl-bg-blue-50= _('Prioritized label')
+ %li.js-priority-badge.inline.gl-mr-3
+ .label-badge.gl-bg-blue-50= _('Prioritized')
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index f95689c0b1d..ce7006001c7 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -9,18 +9,20 @@
#js-promote-label-modal
= render 'shared/labels/nav', labels_or_filters: labels_or_filters, can_admin_label: can_admin_label
- .labels-container.gl-mt-5
- - if can_admin_label && search.blank?
- %p.text-muted
- = _('Labels can be applied to issues and merge requests. Star a label to make it a priority label.')
+ - if can_admin_label && search.blank?
+ %p.text-muted.gl-mt-5
+ = _('Labels can be applied to issues and merge requests. Star a label to make it a priority label.')
+ .labels-container
-# Only show it in the first page
- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1')
- .prioritized-labels.gl-mb-7{ class: [('hide' if hide), ('is-not-draggable' unless can_admin_label)] }
- %h4.gl-mt-3= _('Prioritized Labels')
- %p.text-muted
- = _('Drag to reorder prioritized labels and change their relative priority.')
- .manage-labels-list.js-prioritized-labels{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
+ .prioritized-labels.gl-rounded-base.gl-border.gl-bg-gray-10.gl-mt-4{ class: [('hide' if hide), ('is-not-draggable' unless can_admin_label)] }
+ .gl-px-5.gl-py-4.gl-bg-white.gl-rounded-base.gl-border-b{ class: 'gl-rounded-bottom-left-none! gl-rounded-bottom-right-none!' }
+ %h3.card-title.h5.gl-m-0.gl-relative.gl-line-height-24
+ = _('Prioritized labels')
+ .gl-font-sm.gl-font-weight-semibold.gl-text-gray-500
+ = _('Drag to reorder prioritized labels and change their relative priority.')
+ .js-prioritized-labels.gl-px-3.gl-rounded-base.manage-labels-list{ data: { url: set_priorities_project_labels_path(@project), sortable: can_admin_label } }
#js-priority-labels-empty-state.priority-labels-empty-state{ class: "#{'hidden' unless @prioritized_labels.empty? && search.blank?}" }
= render 'shared/empty_states/priority_labels'
- if @prioritized_labels.any?
@@ -30,16 +32,18 @@
= _('No prioritized labels with such name or description')
- if @labels.any?
- .other-labels
- %h4{ class: ('hide' if hide) }= _('Other Labels')
- .manage-labels-list.js-other-labels
+ .other-labels.gl-rounded-base.gl-border.gl-bg-gray-10.gl-mt-4
+ .gl-px-5.gl-py-4.gl-bg-white.gl-rounded-base.gl-border-b{ class: 'gl-rounded-bottom-left-none! gl-rounded-bottom-right-none!' }
+ %h3.card-title.h5.gl-m-0.gl-relative.gl-line-height-24{ class: ('hide' if hide) }= _('Other labels')
+ .js-other-labels.gl-px-3.gl-rounded-base.manage-labels-list
= render partial: 'shared/label', collection: @labels, as: :label, locals: { subject: @project }
= paginate @labels, theme: 'gitlab'
+
- elsif search.present?
.other-labels
- if @available_labels.any?
%h4
- = _('Other Labels')
+ = _('Other labels')
.nothing-here-block
= _('No other labels with such name or description')
- else
@@ -53,5 +57,5 @@
= render 'shared/empty_states/labels'
%template#js-badge-item-template
- %li.js-priority-badge.inline.gl-ml-3
- .label-badge.gl-bg-blue-50= _('Prioritized label')
+ %li.js-priority-badge.inline.gl-mr-3
+ .label-badge.gl-bg-blue-50= _('Prioritized')
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index a116f69363d..1aac7af443f 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -6,55 +6,59 @@
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
- tooltip_title = label_status_tooltip(label, status) if status
-%li.label-list-item{ id: label_css_id, class: "gl-p-5 gl-border-b", data: { id: label.id } }
- = render "shared/label_row", label: label, force_priority: force_priority
- %ul.label-actions-list
- - if can?(current_user, :admin_label, @project)
- %li.gl-display-inline-block.js-toggle-priority.gl-ml-3{ data: { url: remove_priority_project_label_path(@project, label),
- dom_id: dom_id(label), type: label.type } }
- = render Pajamas::ButtonComponent.new(category: :tertiary,
- icon: 'star',
- button_options: { class: 'remove-priority has-tooltip', 'title': _('Remove priority'), 'aria_label': _('Deprioritize label'), data: { placement: 'bottom' } })
- = render Pajamas::ButtonComponent.new(category: :tertiary,
- icon: 'star-o',
- button_options: { class: 'add-priority has-tooltip', title: _('Prioritize'), aria_label: _('Prioritize label'), data: { placement: 'bottom' } })
- - if current_user
- %li.gl-display-inline-block.label-subscription.js-label-subscription.gl-ml-3
- - if label.can_subscribe_to_label_in_different_levels?
- = render Pajamas::ButtonComponent.new(button_options: { class: "js-unsubscribe-button #{'hidden' if status.unsubscribed?}", data: { url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do
- = _('Unsubscribe')
- .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
- = render Pajamas::ButtonComponent.new(button_options: { data: { toggle: 'dropdown' } }) do
- = _('Subscribe')
- = sprite_icon('chevron-down')
+%li.label-list-item.gl-list-style-none.gl-py-3{ id: label_css_id, data: { id: label.id } }
+ .label-content.gl-px-3.gl-py-2.gl-rounded-base{ class: "#{ 'gl-py-3' if force_priority }" }
+ = render "shared/label_row", label: label, force_priority: force_priority
+ %ul.label-actions-list
+ - if can?(current_user, :admin_label, @project)
+ %li.gl-display-inline-block.js-toggle-priority.gl-ml-3{ data: { url: remove_priority_project_label_path(@project, label),
+ dom_id: dom_id(label), type: label.type } }
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
+ size: :small,
+ icon: 'star',
+ button_options: { class: 'remove-priority has-tooltip', 'title': _('Remove priority'), 'aria_label': _('Deprioritize label'), data: { placement: 'bottom' } })
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
+ size: :small,
+ icon: 'star-o',
+ button_options: { class: 'add-priority has-tooltip', title: _('Prioritize'), aria_label: _('Prioritize label'), data: { placement: 'bottom' } })
+ - if current_user
+ %li.gl-display-inline-block.label-subscription.js-label-subscription.gl-ml-3.gl-mt-1
+ - if label.can_subscribe_to_label_in_different_levels?
+ = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: "js-unsubscribe-button gl-w-full #{'hidden' if status.unsubscribed?}", data: { url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do
+ = _('Unsubscribe')
+ .dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
+ = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: "gl-w-full", data: { toggle: 'dropdown' } }) do
+ = _('Subscribe')
+ = sprite_icon('chevron-down')
+ .dropdown-menu.dropdown-menu-right
+ %ul
+ %li
+ = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_project_label_path(@project, label) } }) do
+ = _('Subscribe at project level')
+ %li
+ = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button js-group-level #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }) do
+ = _('Subscribe at group level')
+ - else
+ = render Pajamas::ButtonComponent.new(size: :small, button_options: { class: 'js-subscribe-button gl-w-full', data: { status: status, url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do
+ = label_subscription_toggle_button_text(label, @project)
+ - if can?(current_user, :admin_label, label)
+ %li.gl-display-inline-block
+ .dropdown
+ = render Pajamas::ButtonComponent.new(category: :tertiary,
+ size: :small,
+ icon: 'ellipsis_v',
+ button_options: { class: 'js-label-options-dropdown gl-ml-3', 'aria_label': _('Label actions dropdown'), title: _('Label actions dropdown'), data: { toggle: 'dropdown' } })
.dropdown-menu.dropdown-menu-right
%ul
%li
- = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_project_label_path(@project, label) } }) do
- = _('Subscribe at project level')
- %li
- = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button js-group-level #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }) do
- = _('Subscribe at group level')
- - else
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-subscribe-button gl-w-full', data: { status: status, url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do
- = label_subscription_toggle_button_text(label, @project)
- - if can?(current_user, :admin_label, label)
- %li.gl-display-inline-block
- .dropdown
- = render Pajamas::ButtonComponent.new(category: :tertiary,
- icon: 'ellipsis_v',
- button_options: { class: 'js-label-options-dropdown gl-ml-3', 'aria_label': _('Label actions dropdown'), title: _('Label actions dropdown'), data: { toggle: 'dropdown' } })
- .dropdown-menu.dropdown-menu-right
- %ul
- %li
- = render Pajamas::ButtonComponent.new(category: :tertiary, href: label.edit_path, variant: :link) do
- = _('Edit')
- - if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group)
+ = render Pajamas::ButtonComponent.new(category: :tertiary, href: label.edit_path, variant: :link) do
+ = _('Edit')
+ - if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group)
+ %li
+ = render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link,
+ button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } }) do
+ = _('Promote to group label')
%li
= render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link,
- button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } }) do
- = _('Promote to group label')
- %li
- = render Pajamas::ButtonComponent.new(category: :tertiary, variant: :link,
- button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }) do
- = _('Delete')
+ button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }) do
+ = _('Delete')
diff --git a/app/views/shared/_label_full_path.html.haml b/app/views/shared/_label_full_path.html.haml
index fd67bbbbd10..b9e5e979ce5 100644
--- a/app/views/shared/_label_full_path.html.haml
+++ b/app/views/shared/_label_full_path.html.haml
@@ -1,4 +1,8 @@
- full_path = label.subject_full_name
-.label-badge.gl-bg-gray-50.gl-max-w-full.gl-text-truncate{ title: full_path }
+.gl-font-sm.gl-font-weight-semibold.gl-text-secondary
+ - if label.project_label?
+ = sprite_icon('project', size: 12, css_class: 'gl-text-gray-600')
+ - else
+ = sprite_icon('group', size: 12, css_class: 'gl-text-gray-600')
= full_path
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index c351ea29c7c..19489981d94 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -3,28 +3,25 @@
- show_label_issues_link = subject_or_group_defined && show_label_issuables_link?(label, :issues)
- show_label_merge_requests_link = subject_or_group_defined && show_label_issuables_link?(label, :merge_requests)
-.label-name.gl-flex-shrink-0.gl-mt-2.gl-mr-5
+.label-name.gl-flex-shrink-0.gl-mr-5
= render_label(label, tooltip: false)
-.label-description.gl-overflow-hidden.gl-w-full
- .gl-display-flex.gl-align-items-stretch.gl-flex-wrap.gl-mt-2
- .gl-flex-basis-half.gl-flex-grow-1.gl-overflow-hidden.gl-mr-5
+ - if show_labels_full_path?(@project, @group)
+ .gl-mt-2
+ = render 'shared/label_full_path', label: label
+.label-description.gl-w-full
+ .gl-display-flex.gl-align-items-stretch.gl-flex-wrap
+ .gl-flex-basis-half.gl-flex-grow-1.gl-mr-5
- if label.description.present?
- = markdown_field(label, :description)
- - elsif show_labels_full_path?(@project, @group)
- = render 'shared/label_full_path', label: label
+ .gl-my-1
+ = markdown_field(label, :description)
%ul.label-links.gl-m-0.gl-p-0.gl-white-space-nowrap
+ - if force_priority
+ %li.js-priority-badge.inline.gl-mr-3.gl-mt-1
+ .label-badge.gl-bg-blue-50= _('Prioritized')
- if show_label_issues_link
- %li.inline
- = link_to_label(label, css_class: 'gl-text-blue-600!') { _('Issues') }
+ %li.inline.gl-my-1
+ = link_to_label(label, css_class: 'gl-mr-5') { _('Issues') }
- if show_label_merge_requests_link
- &middot;
- %li.inline
- = link_to_label(label, type: :merge_request, css_class: 'gl-text-blue-600!') { _('Merge requests') }
+ %li.inline.gl-my-1
+ = link_to_label(label, type: :merge_request) { _('Merge requests') }
= render_if_exists 'shared/label_row_epics_link', label: label
- - if force_priority
- &middot;
- %li.js-priority-badge.inline.gl-ml-3
- .label-badge.gl-bg-blue-50= _('Prioritized label')
- - if label.description.present? && show_labels_full_path?(@project, @group)
- .gl-mt-3
- = render 'shared/label_full_path', label: label
diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml
index d62b2a64e33..b24fa0b3bdb 100644
--- a/app/views/shared/empty_states/_priority_labels.html.haml
+++ b/app/views/shared/empty_states/_priority_labels.html.haml
@@ -1,6 +1,8 @@
-.text-center
+.text-center.gl-mt-1.gl-mb-6
.svg-content{ data: { qa_selector: 'label_svg_content' } }
= image_tag 'illustrations/empty-state/empty-labels-starred-md.svg'
- if can?(current_user, :admin_label, @project)
- %p
- = _("Star labels to start sorting by priority")
+ %div
+ = _("No prioritized labels yet!")
+ %div
+ = _("Star labels to start sorting by priority.")
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 0143cb4c93d..78ab6b5909b 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2757,6 +2757,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: integrations_slack_event
+ :worker_name: Integrations::SlackEventWorker
+ :feature_category: :integrations
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: invalid_gpg_signature_update
:worker_name: InvalidGpgSignatureUpdateWorker
:feature_category: :source_code_management
diff --git a/app/workers/integrations/slack_event_worker.rb b/app/workers/integrations/slack_event_worker.rb
new file mode 100644
index 00000000000..e5cdae1beea
--- /dev/null
+++ b/app/workers/integrations/slack_event_worker.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Integrations
+ class SlackEventWorker
+ include ApplicationWorker
+
+ EVENTS = {
+ 'app_home_opened' => SlackEvents::AppHomeOpenedService
+ }.freeze
+
+ feature_category :integrations
+ data_consistency :delayed
+ urgency :low
+ deduplicate :until_executed
+ idempotent!
+ worker_has_external_dependencies!
+
+ def self.event?(slack_event)
+ EVENTS.key?(slack_event)
+ end
+
+ def perform(args)
+ args = args.with_indifferent_access
+
+ log_extra_metadata_on_done(:slack_event, args[:slack_event])
+ log_extra_metadata_on_done(:slack_user_id, args.dig(:params, :event, :user))
+ log_extra_metadata_on_done(:slack_workspace_id, args.dig(:params, :team_id))
+
+ unless self.class.event?(args[:slack_event])
+ Sidekiq.logger.error(
+ message: 'Unknown slack_event',
+ slack_event: args[:slack_event]
+ )
+
+ return
+ end
+
+ # Ensure idempotency by taking out an exclusive lease keyed to `params.event_id`.
+ # The `event_id` is "a unique identifier for this specific event, globally unique
+ # across all workspaces" and guaranteed to be present as part of the Slack event JSON schema.
+ # See https://api.slack.com/types/event.
+ lease = Gitlab::ExclusiveLease.new("slack_event:#{args[:params][:event_id]}", timeout: 1.hour.to_i)
+ return unless lease.try_obtain
+
+ service_class = EVENTS[args[:slack_event]]
+ response = service_class.new(args[:params]).execute
+
+ lease.cancel if response.error?
+ rescue StandardError => e
+ lease.cancel
+ raise e
+ end
+ end
+end
diff --git a/config/feature_flags/development/search_rate_limited_scopes.yml b/config/feature_flags/development/search_rate_limited_scopes.yml
new file mode 100644
index 00000000000..499a51feca5
--- /dev/null
+++ b/config/feature_flags/development/search_rate_limited_scopes.yml
@@ -0,0 +1,8 @@
+---
+name: search_rate_limited_scopes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118525
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/408521
+milestone: '16.0'
+type: development
+group: group::global search
+default_enabled: false
diff --git a/data/deprecations/15-8-pull-through-cache-container-registry.yml b/data/deprecations/15-8-pull-through-cache-container-registry.yml
index 60a759517e0..6a59dd606e2 100644
--- a/data/deprecations/15-8-pull-through-cache-container-registry.yml
+++ b/data/deprecations/15-8-pull-through-cache-container-registry.yml
@@ -6,4 +6,4 @@
stage: Package # (required) String value of the stage that the feature was created in. e.g., Growth
issue_url: https://gitlab.com/gitlab-org/container-registry/-/issues/842 # (required) Link to the deprecation issue in GitLab
body: | # (required) Do not modify this line, instead modify the lines below.
- The Container Registry pull-through cache is deprecated in GitLab 15.8 and will be removed in GitLab 16.0. While the Container Registry pull-through cache functionality is useful, we have not made significant changes to this feature. You can use the upstream version of the container registry to achieve the same functionality. Removing the pull-through cache allows us also to remove the upstream client code without sacrificing functionality.
+ The Container Registry [pull-through cache](https://docs.docker.com/registry/recipes/mirror/) is deprecated in GitLab 15.8 and will be removed in GitLab 16.0. The pull-through cache is part of the upstream [Docker Distribution project](https://github.com/distribution/distribution). However, we are removing the pull-through cache in favor of the GitLab Dependency Proxy, which allows you to proxy and cache container images from Docker Hub. Removing the pull-through cache allows us also to remove the upstream client code without sacrificing functionality.
diff --git a/data/removals/16_0/16-0-azure-storage-driver-registry.yml b/data/removals/16_0/16-0-azure-storage-driver-registry.yml
new file mode 100644
index 00000000000..029d82a6c08
--- /dev/null
+++ b/data/removals/16_0/16-0-azure-storage-driver-registry.yml
@@ -0,0 +1,12 @@
+#
+- title: "Azure Storage Driver defaults to the correct root prefix" # (required) Clearly explain the change. For example, "The `confidential` field for a `Note` is removed" or "CI/CD job names are limited to 250 characters."
+ announcement_milestone: "15.8" # (required) The milestone when this feature was deprecated.
+ removal_milestone: "16.0" # (required) The milestone when this feature is being removed.
+ breaking_change: true # (required) Change to false if this is not a breaking change.
+ reporter: trizzi # (required) GitLab username of the person reporting the removal
+ stage: Package # (required) String value of the stage that the feature was created in. e.g., Growth
+ issue_url: https://gitlab.com/gitlab-org/container-registry/-/issues/854 # (required) Link to the deprecation issue in GitLab
+ body: | # (required) Do not modify this line, instead modify the lines below.
+ The Azure Storage Driver used to write to `//` as the default root directory. This default root directory appeared in some places in the Azure UI as `/<no-name>/`. We maintained this legacy behavior to support older deployments using this storage driver. However, when moving to Azure from another storage driver, this behavior hides all your data until you configure the storage driver with `trimlegacyrootprefix: true` to build root paths without an extra leading slash.
+
+ In GitLab 16.0, the new default configuration for the storage driver uses `trimlegacyrootprefix: true`, and `/` is the default root directory. You can set your configuration to `trimlegacyrootprefix: false` if needed, to revert to the previous behavior.
diff --git a/db/post_migrate/20230420002547_swap_todos_note_id_to_bigint_for_gitlab_dot_com.rb b/db/post_migrate/20230420002547_swap_todos_note_id_to_bigint_for_gitlab_dot_com.rb
new file mode 100644
index 00000000000..f01ea5c1da2
--- /dev/null
+++ b/db/post_migrate/20230420002547_swap_todos_note_id_to_bigint_for_gitlab_dot_com.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+class SwapTodosNoteIdToBigintForGitlabDotCom < Gitlab::Database::Migration[2.1]
+ include Gitlab::Database::MigrationHelpers::ConvertToBigint
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = 'todos'
+
+ def up
+ return unless should_run?
+
+ swap
+ end
+
+ def down
+ return unless should_run?
+
+ swap
+
+ add_concurrent_index TABLE_NAME, :note_id_convert_to_bigint,
+ name: 'index_todos_on_note_id_convert_to_bigint'
+
+ add_concurrent_foreign_key TABLE_NAME, :notes, column: :note_id_convert_to_bigint,
+ name: 'fk_todos_note_id_convert_to_bigint',
+ on_delete: :cascade, validate: false
+ end
+
+ def swap
+ # This will replace the existing index_todos_on_note_id
+ add_concurrent_index TABLE_NAME, :note_id_convert_to_bigint,
+ name: 'index_todos_on_note_id_convert_to_bigint'
+
+ # This will replace the existing fk_91d1f47b13
+ add_concurrent_foreign_key TABLE_NAME, :notes, column: :note_id_convert_to_bigint,
+ name: 'fk_todos_note_id_convert_to_bigint',
+ on_delete: :cascade
+
+ with_lock_retries(raise_on_exhaustion: true) do
+ execute "LOCK TABLE notes, #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE"
+
+ execute "ALTER TABLE #{TABLE_NAME} RENAME COLUMN note_id TO note_id_tmp"
+ execute "ALTER TABLE #{TABLE_NAME} RENAME COLUMN note_id_convert_to_bigint TO note_id"
+ execute "ALTER TABLE #{TABLE_NAME} RENAME COLUMN note_id_tmp TO note_id_convert_to_bigint"
+
+ function_name = Gitlab::Database::UnidirectionalCopyTrigger
+ .on_table(TABLE_NAME, connection: connection)
+ .name(:note_id, :note_id_convert_to_bigint)
+ execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL"
+
+ execute 'DROP INDEX IF EXISTS index_todos_on_note_id'
+ rename_index TABLE_NAME, 'index_todos_on_note_id_convert_to_bigint',
+ 'index_todos_on_note_id'
+
+ execute "ALTER TABLE #{TABLE_NAME} DROP CONSTRAINT IF EXISTS fk_91d1f47b13"
+ rename_constraint(TABLE_NAME, 'fk_todos_note_id_convert_to_bigint', 'fk_91d1f47b13')
+ end
+ end
+
+ def should_run?
+ com_or_dev_or_test_but_not_jh?
+ end
+end
diff --git a/db/post_migrate/20230422013640_swap_system_note_metadata_note_id_to_bigint_for_gitlab_dot_com.rb b/db/post_migrate/20230422013640_swap_system_note_metadata_note_id_to_bigint_for_gitlab_dot_com.rb
new file mode 100644
index 00000000000..4113bae22e1
--- /dev/null
+++ b/db/post_migrate/20230422013640_swap_system_note_metadata_note_id_to_bigint_for_gitlab_dot_com.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+class SwapSystemNoteMetadataNoteIdToBigintForGitlabDotCom < Gitlab::Database::Migration[2.1]
+ include Gitlab::Database::MigrationHelpers::ConvertToBigint
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = 'system_note_metadata'
+
+ def up
+ return unless should_run?
+
+ swap
+ end
+
+ def down
+ return unless should_run?
+
+ swap
+
+ add_concurrent_index TABLE_NAME, :note_id_convert_to_bigint, unique: true,
+ name: 'index_system_note_metadata_on_note_id_convert_to_bigint'
+
+ add_concurrent_foreign_key TABLE_NAME, :notes,
+ column: :note_id_convert_to_bigint,
+ name: :fk_system_note_metadata_note_id_convert_to_bigint,
+ on_delete: :cascade,
+ validate: false
+ end
+
+ def swap
+ # This will replace the existing index_system_note_metadata_on_note_id
+ add_concurrent_index TABLE_NAME, :note_id_convert_to_bigint, unique: true,
+ name: 'index_system_note_metadata_on_note_id_convert_to_bigint'
+
+ # This will replace the existing fk_d83a918cb1
+ add_concurrent_foreign_key TABLE_NAME, :notes, column: :note_id_convert_to_bigint,
+ name: 'fk_system_note_metadata_note_id_convert_to_bigint',
+ on_delete: :cascade
+
+ with_lock_retries(raise_on_exhaustion: true) do
+ execute "LOCK TABLE notes, #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE"
+
+ execute "ALTER TABLE #{TABLE_NAME} RENAME COLUMN note_id TO note_id_tmp"
+ execute "ALTER TABLE #{TABLE_NAME} RENAME COLUMN note_id_convert_to_bigint TO note_id"
+ execute "ALTER TABLE #{TABLE_NAME} RENAME COLUMN note_id_tmp TO note_id_convert_to_bigint"
+
+ function_name = Gitlab::Database::UnidirectionalCopyTrigger
+ .on_table(TABLE_NAME, connection: connection)
+ .name(:note_id, :note_id_convert_to_bigint)
+ execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL"
+
+ # Swap defaults
+ change_column_default TABLE_NAME, :note_id, nil
+ change_column_default TABLE_NAME, :note_id_convert_to_bigint, 0
+
+ execute 'DROP INDEX IF EXISTS index_system_note_metadata_on_note_id'
+ rename_index TABLE_NAME, 'index_system_note_metadata_on_note_id_convert_to_bigint',
+ 'index_system_note_metadata_on_note_id'
+
+ execute "ALTER TABLE #{TABLE_NAME} DROP CONSTRAINT IF EXISTS fk_d83a918cb1"
+ rename_constraint(TABLE_NAME, 'fk_system_note_metadata_note_id_convert_to_bigint', 'fk_d83a918cb1')
+ end
+ end
+
+ def should_run?
+ com_or_dev_or_test_but_not_jh?
+ end
+end
diff --git a/db/schema_migrations/20230420002547 b/db/schema_migrations/20230420002547
new file mode 100644
index 00000000000..3631fb2e72d
--- /dev/null
+++ b/db/schema_migrations/20230420002547
@@ -0,0 +1 @@
+a2f9f863a50f908e67e35f2b3f73ba7b228ed74ea0efe67425bd9c266c94a84c \ No newline at end of file
diff --git a/db/schema_migrations/20230422013640 b/db/schema_migrations/20230422013640
new file mode 100644
index 00000000000..7a26cccb409
--- /dev/null
+++ b/db/schema_migrations/20230422013640
@@ -0,0 +1 @@
+4bc5d5e45c6624f0931a45e2515219bd0a89a16eb55e87763366954dec214e46 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index f478a53e52e..47569c39a8d 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -22976,13 +22976,13 @@ ALTER SEQUENCE suggestions_id_seq OWNED BY suggestions.id;
CREATE TABLE system_note_metadata (
id integer NOT NULL,
- note_id integer NOT NULL,
+ note_id_convert_to_bigint integer DEFAULT 0 NOT NULL,
commit_count integer,
action character varying,
created_at timestamp without time zone NOT NULL,
updated_at timestamp without time zone NOT NULL,
description_version_id bigint,
- note_id_convert_to_bigint bigint DEFAULT 0 NOT NULL
+ note_id bigint NOT NULL
);
CREATE SEQUENCE system_note_metadata_id_seq
@@ -23163,11 +23163,11 @@ CREATE TABLE todos (
state character varying NOT NULL,
created_at timestamp without time zone,
updated_at timestamp without time zone,
- note_id integer,
+ note_id_convert_to_bigint integer,
commit_id character varying,
group_id integer,
resolved_by_action smallint,
- note_id_convert_to_bigint bigint
+ note_id bigint
);
CREATE SEQUENCE todos_id_seq
@@ -32535,8 +32535,6 @@ CREATE UNIQUE INDEX index_system_note_metadata_on_description_version_id ON syst
CREATE UNIQUE INDEX index_system_note_metadata_on_note_id ON system_note_metadata USING btree (note_id);
-CREATE UNIQUE INDEX index_system_note_metadata_on_note_id_convert_to_bigint ON system_note_metadata USING btree (note_id_convert_to_bigint);
-
CREATE INDEX index_taggings_on_tag_id ON taggings USING btree (tag_id);
CREATE INDEX index_taggings_on_taggable_id_and_taggable_type_and_context ON taggings USING btree (taggable_id, taggable_type, context);
@@ -32595,8 +32593,6 @@ CREATE INDEX index_todos_on_group_id ON todos USING btree (group_id);
CREATE INDEX index_todos_on_note_id ON todos USING btree (note_id);
-CREATE INDEX index_todos_on_note_id_convert_to_bigint ON todos USING btree (note_id_convert_to_bigint);
-
CREATE INDEX index_todos_on_project_id_and_id ON todos USING btree (project_id, id);
CREATE INDEX index_todos_on_target_type_and_target_id ON todos USING btree (target_type, target_id);
@@ -37338,9 +37334,6 @@ ALTER TABLE ONLY integrations
ALTER TABLE ONLY merge_requests
ADD CONSTRAINT fk_source_project FOREIGN KEY (source_project_id) REFERENCES projects(id) ON DELETE SET NULL;
-ALTER TABLE ONLY system_note_metadata
- ADD CONSTRAINT fk_system_note_metadata_note_id_convert_to_bigint FOREIGN KEY (note_id_convert_to_bigint) REFERENCES notes(id) ON DELETE CASCADE NOT VALID;
-
ALTER TABLE ONLY timelogs
ADD CONSTRAINT fk_timelogs_issues_issue_id FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
@@ -37350,9 +37343,6 @@ ALTER TABLE ONLY timelogs
ALTER TABLE ONLY timelogs
ADD CONSTRAINT fk_timelogs_note_id FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE SET NULL;
-ALTER TABLE ONLY todos
- ADD CONSTRAINT fk_todos_note_id_convert_to_bigint FOREIGN KEY (note_id_convert_to_bigint) REFERENCES notes(id) ON DELETE CASCADE NOT VALID;
-
ALTER TABLE ONLY u2f_registrations
ADD CONSTRAINT fk_u2f_registrations_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 8031e2f4896..a8736b3ed1d 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -718,6 +718,8 @@ To solve this:
### Very large repositories never successfully synchronize on the **secondary** site
+#### GitLab 10.1 and earlier
+
GitLab places a timeout on all repository clones, including project imports
and Geo synchronization operations. If a fresh `git clone` of a repository
on the **primary** takes more than the default three hours, you may be affected by this.
@@ -740,6 +742,10 @@ add the following line to `/etc/gitlab/gitlab.rb`:
This increases the timeout to four hours (14400 seconds). Choose a time
long enough to accommodate a full clone of your largest repositories.
+#### GitLab 10.2 and later
+
+Geo [replicates Git repositories over HTTPS](../index.md#how-it-works). GitLab does not place a timeout on these requests. If a Git repository is failing to replicate, with a consistent job execution time, then you should check for timeouts applied by external components such as load balancers.
+
### New LFS objects are never replicated
If new LFS objects are never replicated to secondary Geo sites, check the version of
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index f0702288da5..68d8f3f04b8 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -157,16 +157,17 @@ Set the limit to `0` to disable it.
### Search rate limit
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80631) in GitLab 14.9.
-> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104208) to include issue, merge request, and epic searches to the rate limit in GitLab 15.9.
+> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/104208) in GitLab 15.9 to include issue, merge request, and epic searches in the rate limit.
+> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118525) in GitLab 16.0 to apply rate limits to [search scopes](../user/search/index.md#global-search-scopes) for authenticated requests.
This setting limits search requests as follows:
| Limit | Default (requests per minute) |
|-------------------------|-------------------------------|
-| Authenticated user | 30 |
-| Unauthenticated user | 10 |
+| Authenticated user | 300 |
+| Unauthenticated user | 100 |
-Depending on the number of enabled [scopes](../user/search/index.md#global-search-scopes), a global search request can consume two to seven requests per minute. You may want to disable one or more scopes to use fewer requests. Search requests that exceed the search rate limit per minute return the following error:
+Search requests that exceed the search rate limit per minute return the following error:
```plaintext
This endpoint has been requested too many times. Try again later.
diff --git a/doc/administration/silent_mode/index.md b/doc/administration/silent_mode/index.md
index f07f1dde323..e98aaaf4e0a 100644
--- a/doc/administration/silent_mode/index.md
+++ b/doc/administration/silent_mode/index.md
@@ -4,7 +4,7 @@ group: Geo
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
-# GitLab Silent Mode (Alpha) **(FREE SELF)**
+# GitLab Silent Mode (Experiment) **(FREE SELF)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/9826) in GitLab 15.11. This feature is an [Experiment](../../policy/alpha-beta-support.md#experiment).
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index a054aab5dd6..d030c6a8cb4 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -18476,7 +18476,6 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="pipelinesecurityreportfindinglinks"></a>`links` | [`[VulnerabilityLink!]`](#vulnerabilitylink) | List of links associated with the vulnerability. |
| <a id="pipelinesecurityreportfindinglocation"></a>`location` | [`VulnerabilityLocation`](#vulnerabilitylocation) | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. |
| <a id="pipelinesecurityreportfindingmergerequest"></a>`mergeRequest` | [`MergeRequest`](#mergerequest) | Merge request that fixes the vulnerability. |
-| <a id="pipelinesecurityreportfindingname"></a>`name` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.1. Use `title`. |
| <a id="pipelinesecurityreportfindingproject"></a>`project` | [`Project`](#project) | Project on which the vulnerability finding was found. |
| <a id="pipelinesecurityreportfindingprojectfingerprint"></a>`projectFingerprint` **{warning-solid}** | [`String`](#string) | **Deprecated** in 15.1. The `project_fingerprint` attribute is being deprecated. Use `uuid` to identify findings. |
| <a id="pipelinesecurityreportfindingremediations"></a>`remediations` | [`[VulnerabilityRemediationType!]`](#vulnerabilityremediationtype) | Remediations of the security report finding. |
diff --git a/doc/api/graphql/removed_items.md b/doc/api/graphql/removed_items.md
index b4e14dec219..3b9526dc283 100644
--- a/doc/api/graphql/removed_items.md
+++ b/doc/api/graphql/removed_items.md
@@ -16,17 +16,11 @@ Fields removed in GitLab 16.0.
### GraphQL Fields
-[Removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111750) in GitLab 16.0:
-
-| Field name | GraphQL type | Deprecated in | Use instead |
-|----------------------|--------------------------|---------------|----------------------------|
-| `external` | `GraphQL::Types::Boolean`| 15.9 | None |
-
-[Removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118617) in GitLab 16.0:
-
-| Field name | GraphQL type | Deprecated in | Use instead |
-|----------------------|--------------------------|---------------|----------------------------|
-| `confidence` | `GraphQL::Types::String` | 15.4 | None |
+| Field name | GraphQL type | Deprecated in | Removal MR | Use instead |
+|---|---|---|---|---|
+| `name` | `PipelineSecurityReportFinding` | [15.1](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89571) | [!119055](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119055) | `title` |
+| `external` | `ReleaseAssetLink` | 15.9 | [!111750](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111750) | None |
+| `confidence` | `PipelineSecurityReportFinding` | 15.4 | [!118617](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118617) | None |
### GraphQL Mutations
diff --git a/doc/ci/pipeline_editor/index.md b/doc/ci/pipeline_editor/index.md
index 1a7155c8195..d920c34d90a 100644
--- a/doc/ci/pipeline_editor/index.md
+++ b/doc/ci/pipeline_editor/index.md
@@ -21,7 +21,7 @@ From the pipeline editor page you can:
added with the [`include`](../yaml/index.md#include) keyword.
- View a [list of the CI/CD configuration added with the `include` keyword](#view-included-cicd-configuration).
- See a [visualization](#visualize-ci-configuration) of the current configuration.
-- View an [expanded](#view-expanded-configuration) version of your configuration.
+- View the [full configuration](#view-full-configuration), which displays the configuration with any configuration from `include` added.
- [Commit](#commit-changes-to-ci-configuration) the changes to a specific branch.
In GitLab 13.9 and earlier, you must already have [a `.gitlab-ci.yml` file](../quick_start/index.md#create-a-gitlab-ciyml-file)
@@ -95,13 +95,14 @@ Hover over a job to highlight its `needs` relationships:
If the configuration does not have any `needs` relationships, then no lines are drawn because
each job depends only on the previous stage being completed successfully.
-## View expanded configuration
+## View full configuration
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/246801) in GitLab 13.9.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/301103) in GitLab 13.12.
+> - **View merged YAML** tab [renamed to **Full configuration**](https://gitlab.com/gitlab-org/gitlab/-/issues/377404) in GitLab 16.0.
To view the fully expanded CI/CD configuration as one combined file, go to the
-pipeline editor's **View merged YAML** tab. This tab displays an expanded configuration
+pipeline editor's **Full configuration** tab. This tab displays an expanded configuration
where:
- Configuration imported with [`include`](../yaml/index.md#include) is copied into the view.
@@ -130,7 +131,7 @@ fully expanded version are both valid:
- pyflakes python/
```
-- Expanded configuration in **View merged YAML** tab:
+- Expanded configuration in **Full configuration** tab:
```yaml
".python-req":
@@ -169,7 +170,7 @@ It can happen when:
- The syntax status on the **Edit** tab (valid or invalid).
- The **Visualize** tab.
- The **Lint** tab.
- - The **View merged YAML** tab.
+ - The **Full configuration** tab.
You can still work on your CI/CD configuration and commit the changes you made without
any issues. As soon as the service becomes available again, the syntax validation
diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md
index cf3af8f1810..973c6b90fc5 100644
--- a/doc/ci/troubleshooting.md
+++ b/doc/ci/troubleshooting.md
@@ -313,7 +313,7 @@ likely to hit the default memory limit.
To reduce the configuration size, you can:
- Check the length of the expanded CI/CD configuration in the pipeline editor's
- [merged YAML](pipeline_editor/index.md#view-expanded-configuration) tab. Look for
+ [Full configuration](pipeline_editor/index.md#view-full-configuration) tab. Look for
duplicated configuration that can be removed or simplified.
- Move long or repeated `script` sections into standalone scripts in the project.
- Use [parent and child pipelines](pipelines/downstream_pipelines.md#parent-child-pipelines) to move some
diff --git a/doc/development/gemfile.md b/doc/development/gemfile.md
index d4dc66bae8f..add93e37024 100644
--- a/doc/development/gemfile.md
+++ b/doc/development/gemfile.md
@@ -31,6 +31,13 @@ export BUNDLER_CHECKSUM_VERIFICATION_OPT_IN=1
bundle install
```
+Setting it to `false` can also disable it:
+
+```shell
+export BUNDLER_CHECKSUM_VERIFICATION_OPT_IN=false
+bundle install
+```
+
### Updating the checksum file
This needs to be done for any new, or updated gems.
diff --git a/doc/development/testing_guide/flaky_tests.md b/doc/development/testing_guide/flaky_tests.md
index 8976bbf8691..e41c680e0ee 100644
--- a/doc/development/testing_guide/flaky_tests.md
+++ b/doc/development/testing_guide/flaky_tests.md
@@ -116,6 +116,8 @@ Adding a delay in API or controller could help reproducing the issue.
time before throwing an `element not found` error.
- [Example 2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101728/diffs): A CSS selector
only appears after a GraphQL requests has finished, and the UI has updated.
+- [Example 3](https://gitlab.com/gitlab-org/gitlab/-/issues/408215): A false-positive test, Capybara imediatly returns true after
+page visit and page is not fully loaded, or if the element is not detectable by webdriver (such as being rendered outside the viewport or behind other elements).
### Datetime-sensitive
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 9f636b4f2af..aa3e02abc29 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -3120,7 +3120,7 @@ such as the following:
| Encrypt SAML assertion | Optional | Uses TLS between your identity provider, the user's browser, and GitLab. |
| Sign SAML assertion | Optional | Validates the integrity of a SAML assertion. When active, signs the whole response. |
| Check SAML request signature | Optional | Checks the signature on the SAML response. |
-| Default RelayState | Optional | Specifies the URL users should end up on after successfully signing in through SAML at your IdP. |
+| Default RelayState | Optional | Specifies the sub-paths of the base URL that users should end up on after successfully signing in through SAML at your IdP. |
| NameID format | Persistent | See [NameID format details](../user/group/saml_sso/index.md#manage-user-saml-identity). |
| Additional URLs | Optional | May include the issuer, identifier, or assertion consumer service URL in other fields on some providers. |
diff --git a/doc/subscriptions/customers_portal.md b/doc/subscriptions/customers_portal.md
index 3f493f255dc..07e6e952419 100644
--- a/doc/subscriptions/customers_portal.md
+++ b/doc/subscriptions/customers_portal.md
@@ -8,9 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
For some management tasks for your subscription and account, you use the Customers Portal.
-The Customers Portal is available to customers who purchased their
-subscription from GitLab. If you made your purchase through a partner or
-reseller, contact them directly for assistance with your subscription.
+If you made your purchase through an authorized reseller, you must contact them directly to make changes to your subscription (your subscriptions are read-only).
You can also specifically manage your [GitLab SaaS subscription](gitlab_com/index.md)
or [self-managed subscription](self_managed/index.md).
@@ -88,9 +86,6 @@ Customers are required to use their GitLab.com account to register for a new Cus
If you have a legacy Customers Portal account that is not linked to a GitLab.com account, you may still [sign in](https://customers.gitlab.com/customers/sign_in?legacy=true) using an email and password. However, you should [create](https://gitlab.com/users/sign_up) and [link a GitLab.com account](#change-the-linked-account) to ensure continued access to the Customers Portal.
-Customers of resellers do not have access to this portal and should contact their reseller for any
-changes to their subscription.
-
To change the GitLab.com account linked to your Customers Portal account:
1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in).
diff --git a/doc/subscriptions/gitlab_com/index.md b/doc/subscriptions/gitlab_com/index.md
index 4d11a6fffc8..90ae556ce6b 100644
--- a/doc/subscriptions/gitlab_com/index.md
+++ b/doc/subscriptions/gitlab_com/index.md
@@ -240,14 +240,22 @@ To change the namespace linked to a subscription:
[linked](../customers_portal.md#link-a-gitlabcom-account) GitLab.com account.
1. Go to the **Manage Purchases** page.
1. Select **Change linked namespace**.
-1. Select the desired group from the **This subscription is for** dropdown list. For a group to appear here, you must have the Owner role for that group.
-1. Select **Proceed to checkout**.
+1. Select the desired group from the **New Namespace** dropdown list. For a group to appear here, you must have the Owner role for that group.
+1. If the [total number of users](#view-seat-usage) in your group exceeds the number of seats in your subscription,
+ you are prompted to pay for the additional users. Subscription charges are calculated based on
+ the total number of users in a group, including its subgroups and nested projects.
+
+ If you purchased your subscription through an authorized reseller, you are unable to pay for additional users.
+ You can either:
+
+ - Remove additional users, so that no overage is detected.
+ - Contact the partner to purchase additional seats now or at the end of your subscription term.
+
+1. Select **Confirm changes**.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For a demo, see [Linking GitLab Subscription to the Namespace](https://youtu.be/qAq8pyFP-a0).
-Subscription charges are calculated based on the total number of users in a group, including its subgroups and nested projects. If the [total number of users](#view-seat-usage) exceeds the number of seats in your subscription, your account is charged for the additional users and you need to pay for the overage before you can change the linked namespace.
-
Only one namespace can be linked to a subscription.
## Upgrade your GitLab SaaS subscription tier
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index 9e9c906c9eb..abe33fd04d8 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -728,7 +728,7 @@ config file locations instead, for example `config/redis.cache.yml` or
- [Breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/)
</div>
-The Container Registry pull-through cache is deprecated in GitLab 15.8 and will be removed in GitLab 16.0. While the Container Registry pull-through cache functionality is useful, we have not made significant changes to this feature. You can use the upstream version of the container registry to achieve the same functionality. Removing the pull-through cache allows us also to remove the upstream client code without sacrificing functionality.
+The Container Registry [pull-through cache](https://docs.docker.com/registry/recipes/mirror/) is deprecated in GitLab 15.8 and will be removed in GitLab 16.0. The pull-through cache is part of the upstream [Docker Distribution project](https://github.com/distribution/distribution). However, we are removing the pull-through cache in favor of the GitLab Dependency Proxy, which allows you to proxy and cache container images from Docker Hub. Removing the pull-through cache allows us also to remove the upstream client code without sacrificing functionality.
</div>
diff --git a/doc/update/removals.md b/doc/update/removals.md
index a7cde5bbc74..b92c90379cc 100644
--- a/doc/update/removals.md
+++ b/doc/update/removals.md
@@ -49,6 +49,16 @@ change supports production deployments that require more robust database managem
If you want Auto DevOps to provision an in-cluster database,
set the `POSTGRES_ENABLED` CI/CD variable to `true`.
+### Azure Storage Driver defaults to the correct root prefix
+
+WARNING:
+This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/).
+Review the details carefully before upgrading.
+
+The Azure Storage Driver used to write to `//` as the default root directory. This default root directory appeared in some places in the Azure UI as `/<no-name>/`. We maintained this legacy behavior to support older deployments using this storage driver. However, when moving to Azure from another storage driver, this behavior hides all your data until you configure the storage driver with `trimlegacyrootprefix: true` to build root paths without an extra leading slash.
+
+In GitLab 16.0, the new default configuration for the storage driver uses `trimlegacyrootprefix: true`, and `/` is the default root directory. You can set your configuration to `trimlegacyrootprefix: false` if needed, to revert to the previous behavior.
+
### Project REST API field `operations_access_level` removed
WARNING:
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index d6b1d243d82..27af64cd0e8 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -279,7 +279,7 @@ use a template from:
The project CI/CD configuration merges into the required pipeline configuration when
a pipeline runs. The merged configuration is the same as if the required pipeline configuration
added the project configuration with the [`include` keyword](../../../ci/yaml/index.md#include).
-To view a project's full merged configuration, [View the merged YAML](../../../ci/pipeline_editor/index.md#view-expanded-configuration)
+To view a project's full merged configuration, [View full configuration](../../../ci/pipeline_editor/index.md#view-full-configuration)
in the pipeline editor.
To select a CI/CD template for the required pipeline configuration:
diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md
index 7dee92f0c29..293884daa03 100644
--- a/doc/user/project/description_templates.md
+++ b/doc/user/project/description_templates.md
@@ -46,10 +46,11 @@ and see if you can find your description template in the **Choose a template** d
## Create a merge request template
Similarly to issue templates, create a new Markdown (`.md`) file inside the
-`.gitlab/merge_request_templates/` directory in your repository. Commit and
-push to your default branch.
+`.gitlab/merge_request_templates/` directory in your repository. Unlike issue
+templates, merge requests have [additional inheritance rules](merge_requests/creating_merge_requests.md)
+that depend on the contents of commit messages and branch names.
-To create a merge request description template:
+To create a merge request description template for a project:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Repository**.
@@ -99,7 +100,7 @@ your merge request template with their values:
| `%{all_commits}` | Messages from all commits in the merge request. Limited to 100 most recent commits. Skips commit bodies exceeding 100 KiB and merge commit messages. | `* Feature introduced` <br><br> `This commit implements feature` <br> `Changelog:added` <br><br> `* Bug fixed` <br><br> `* Documentation improved` <br><br>`This commit introduced better docs.` |
| `%{co_authored_by}` | Names and emails of commit authors in a `Co-authored-by` Git commit trailer format. Limited to authors of 100 most recent commits in merge request. | `Co-authored-by: Zane Doe <zdoe@example.com>` <br> `Co-authored-by: Blake Smith <bsmith@example.com>` |
| `%{first_commit}` | Full message of the first commit in merge request diff. | `Update README.md` |
-| `%{first_multiline_commit}` | Full message of the first commit that's not a merge commit and has more than one line in message body. Merge request title if all commits aren't multiline. | `Update README.md`<br><br>`Improved project description in readme file.` |
+| `%{first_multiline_commit}` | Full message of the first commit that's not a merge commit and has more than one line in message body. Merge request title if all commits aren't multiline. | `Update README.md` <br><br> `Improved project description in readme file.` |
| `%{source_branch}` | The name of the branch being merged. | `my-feature-branch` |
| `%{target_branch}` | The name of the branch that the changes are applied to. | `main` |
@@ -180,7 +181,7 @@ You can also provide `issues_template` and `merge_requests_template` attributes
#### Priority of default description templates
-When you set [merge request and issue description templates](#set-a-default-template-for-merge-requests-and-issues)
+When you set [issue description templates](#set-a-default-template-for-merge-requests-and-issues)
in various places, they have the following priorities in a project.
The ones higher up override the ones below:
@@ -188,6 +189,9 @@ The ones higher up override the ones below:
1. `Default.md` (case insensitive) from the parent group.
1. `Default.md` (case insensitive) from the project repository.
+Merge requests have [additional inheritance rules](merge_requests/creating_merge_requests.md)
+that depend on the contents of commit messages and branch names.
+
## Example description template
We use description templates for issues and merge requests in the
diff --git a/doc/user/project/merge_requests/creating_merge_requests.md b/doc/user/project/merge_requests/creating_merge_requests.md
index 15e760e3696..a91d324016a 100644
--- a/doc/user/project/merge_requests/creating_merge_requests.md
+++ b/doc/user/project/merge_requests/creating_merge_requests.md
@@ -11,7 +11,7 @@ GitLab provides many different ways to create a merge request.
NOTE:
GitLab enforces [branch naming rules](../repository/branches/index.md#name-your-branch)
-to prevent problems, and provides
+to prevent problems, and provides
[branch naming patterns](../repository/branches/index.md#prefix-branch-names-with-issue-numbers)
to streamline merge request creation.
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index ee7f4e5dfed..01bc348c304 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -26,6 +26,22 @@ view [this GitLab Flow video](https://www.youtube.com/watch?v=InKNIvky2KE).
Learn the various ways to [create a merge request](creating_merge_requests.md).
+### Use merge request templates
+
+When you create a merge request, GitLab checks for the existence of a
+[description template](../description_templates.md) to add data to your merge request.
+GitLab checks these locations in order from 1 to 5, and applies the first template
+found to your merge request:
+
+| Name | Project UI<br>setting | Group<br>`default.md` | Instance<br>`default.md` | Project<br>`default.md` | No template |
+| :-- | :--: | :--: | :--: | :--: | :--: |
+| Standard commit message | 1 | 2 | 3 | 4 | 5 |
+| Commit message with an [issue closing pattern](../issues/managing_issues.md#closing-issues-automatically) like `Closes #1234` | 1 | 2 | 3 | 4 | 5 \* |
+| Branch name [prefixed with an issue ID](../repository/branches/index.md#prefix-branch-names-with-issue-numbers), like `1234-example` | 1 \* | 2 \* | 3 \* | 4 \* | 5 \* |
+
+NOTE:
+Items marked with an asterisk (\*) also append an [issue closing pattern](../issues/managing_issues.md#closing-issues-automatically).
+
## View merge requests
You can view merge requests for your project, group, or yourself.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 1fd4940d67f..d555c14504b 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -234,6 +234,8 @@ module API
mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
mount ::API::Integrations
+ mount ::API::Integrations::Slack::Events
+ mount ::API::Integrations::Slack::Interactions
mount ::API::Integrations::Slack::Options
mount ::API::Integrations::JiraConnect::Subscriptions
mount ::API::Invitations
diff --git a/lib/api/integrations/slack/events.rb b/lib/api/integrations/slack/events.rb
new file mode 100644
index 00000000000..7a0687a06b6
--- /dev/null
+++ b/lib/api/integrations/slack/events.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+# This API endpoint handles all events sent from Slack once a Slack
+# workspace has installed the GitLab Slack app.
+#
+# See https://api.slack.com/apis/connections/events-api.
+module API
+ class Integrations
+ module Slack
+ class Events < ::API::Base
+ include Slack::Concerns::VerifiesRequest
+
+ feature_category :integrations
+
+ namespace 'integrations/slack' do
+ desc 'Receive Slack events' do
+ success [
+ { code: 200, message: 'Successfully processed event' },
+ { code: 204, message: 'Failed to process event' }
+ ]
+ failure [
+ { code: 401, message: 'Unauthorized' }
+ ]
+ end
+
+ # Params are based on the JSON schema spec for Slack events https://api.slack.com/types/event.
+ # We mark all params as `optional` as we never want to fail a request from Slack. Slack may remove
+ # deprecated params in future that are currently described in their JSON schema spec as required.
+ params do
+ optional :token, type: String, desc: '(Deprecated by Slack) The request token, unused by GitLab'
+ optional :team_id, type: String, desc: 'The Slack workspace ID of where the event occurred'
+ optional :api_app_id, type: String, desc: 'The Slack app ID'
+ optional :event, type: Hash, desc: 'The event object with variable properties'
+ optional :type, type: String, desc: 'The kind of event this is, usually `event_callback`'
+ optional :event_id, type: String, desc: 'A unique identifier for this specific event'
+ optional :event_time, type: Integer, desc: 'The epoch timestamp in seconds when this event was dispatched'
+ optional :authed_users, type: Array[String], desc: '(Deprecated by Slack) An array of Slack user IDs'
+ end
+
+ post :events do
+ response = ::Integrations::SlackEventService.new(params).execute
+
+ status :ok
+
+ response.payload
+ rescue StandardError => e
+ # Track the error, but respond with a `2xx` because we don't want to risk
+ # Slack rate-limiting, or disabling our app, due to error responses.
+ # See https://api.slack.com/apis/connections/events-api.
+ Gitlab::ErrorTracking.track_exception(e)
+
+ no_content!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/integrations/slack/interactions.rb b/lib/api/integrations/slack/interactions.rb
new file mode 100644
index 00000000000..af8331977f3
--- /dev/null
+++ b/lib/api/integrations/slack/interactions.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module API
+ # This API endpoint handles interaction payloads sent from Slack.
+ # See https://api.slack.com/interactivity/handling.
+ class Integrations
+ module Slack
+ class Interactions < ::API::Base
+ include Slack::Concerns::VerifiesRequest
+
+ feature_category :integrations
+
+ namespace 'integrations/slack' do
+ post :interactions, urgency: :low do
+ service_params = Gitlab::Json.parse(params[:payload]).deep_symbolize_keys!
+ response = ::Integrations::SlackInteractionService.new(service_params).execute
+
+ status :ok
+
+ response.payload
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(e)
+
+ no_content!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/integrations/slack/options.rb b/lib/api/integrations/slack/options.rb
index 3c01624a168..58e61b8cee0 100644
--- a/lib/api/integrations/slack/options.rb
+++ b/lib/api/integrations/slack/options.rb
@@ -6,7 +6,7 @@ module API
class Integrations
module Slack
class Options < ::API::Base
- include ::API::Integrations::Slack::Concerns::VerifiesRequest
+ include Slack::Concerns::VerifiesRequest
feature_category :integrations
diff --git a/lib/banzai/filter/asset_proxy_filter.rb b/lib/banzai/filter/asset_proxy_filter.rb
index 13b86277ce1..00ffdd3d809 100644
--- a/lib/banzai/filter/asset_proxy_filter.rb
+++ b/lib/banzai/filter/asset_proxy_filter.rb
@@ -6,11 +6,35 @@ module Banzai
# as well as hiding the customer's IP address when requesting images.
# Copies the original img `src` to `data-canonical-src` then replaces the
# `src` with a new url to the proxy server.
- class AssetProxyFilter < HTML::Pipeline::CamoFilter
+ #
+ # Based on https://github.com/gjtorikian/html-pipeline/blob/v2.14.3/lib/html/pipeline/camo_filter.rb
+ class AssetProxyFilter < HTML::Pipeline::Filter
def initialize(text, context = nil, result = nil)
super
end
+ def call
+ return doc unless asset_proxy_enabled?
+
+ doc.search('img').each do |element|
+ original_src = element['src']
+ next unless original_src
+
+ begin
+ uri = URI.parse(original_src)
+ rescue StandardError
+ next
+ end
+
+ next if uri.host.nil? && !original_src.start_with?('///')
+ next if asset_host_allowed?(uri.host)
+
+ element['src'] = asset_proxy_url(original_src)
+ element['data-canonical-src'] = original_src
+ end
+ doc
+ end
+
def validate
needs(:asset_proxy, :asset_proxy_secret_key) if asset_proxy_enabled?
end
@@ -63,6 +87,24 @@ module Banzai
application_settings.try(:asset_proxy_whitelist).presence ||
[Gitlab.config.gitlab.host]
end
+
+ private
+
+ def asset_proxy_enabled?
+ !context[:disable_asset_proxy]
+ end
+
+ def asset_proxy_url(url)
+ "#{context[:asset_proxy]}/#{asset_url_hash(url)}/#{hexencode(url)}"
+ end
+
+ def asset_url_hash(url)
+ OpenSSL::HMAC.hexdigest('sha1', context[:asset_proxy_secret_key], url)
+ end
+
+ def hexencode(str)
+ str.unpack1('H*')
+ end
end
end
end
diff --git a/lib/banzai/filter/commit_trailers_filter.rb b/lib/banzai/filter/commit_trailers_filter.rb
index 817bea42757..c0160680a61 100644
--- a/lib/banzai/filter/commit_trailers_filter.rb
+++ b/lib/banzai/filter/commit_trailers_filter.rb
@@ -70,7 +70,7 @@ module Banzai
#
# Returns a String with a link to the user.
def link_to_user_or_email(name, email, trailer)
- link_to_user User.find_by_any_email(email),
+ link_to_user User.with_public_email(email).first,
name: name,
email: email,
trailer: trailer
diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb
index e8f13a92ee7..fa7c4972c91 100644
--- a/lib/gitlab/checks/branch_check.rb
+++ b/lib/gitlab/checks/branch_check.rb
@@ -42,7 +42,7 @@ module Gitlab
def prohibited_branch_checks
return if deletion?
- if branch_name =~ /\A\h{40}\z/
+ if branch_name =~ %r{\A\h{40}(/|\z)}
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_hex_branch_name]
end
end
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index aeadc89095b..7a4c65f8c5b 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.31.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.32.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
index aeadc89095b..7a4c65f8c5b 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.31.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.32.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
index e336f69a7f6..48ec7577b6a 100644
--- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.48.0'
+ DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.48.1'
.dast-auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${DAST_AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index ea6216a9210..e535b9e9ffc 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.48.0'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.48.1'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
index 34560600c10..ed16b4861b3 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_DEPLOY_IMAGE_VERSION: 'v2.48.0'
+ AUTO_DEPLOY_IMAGE_VERSION: 'v2.48.1'
.auto-deploy:
image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}"
diff --git a/lib/gitlab/database/dynamic_model_helpers.rb b/lib/gitlab/database/dynamic_model_helpers.rb
index 2deb89a0b84..83edf77f37e 100644
--- a/lib/gitlab/database/dynamic_model_helpers.rb
+++ b/lib/gitlab/database/dynamic_model_helpers.rb
@@ -17,7 +17,7 @@ module Gitlab
klass
end
- def each_batch(table_name, connection:, scope: ->(table) { table.all }, of: BATCH_SIZE)
+ def each_batch(table_name, connection:, scope: ->(table) { table.all }, of: BATCH_SIZE, **opts)
if transaction_open?
raise <<~MSG.squish
each_batch should not run inside a transaction, you can disable
@@ -26,13 +26,21 @@ module Gitlab
MSG
end
- scope.call(define_batchable_model(table_name, connection: connection))
- .each_batch(of: of) { |batch| yield batch }
+ opts.select! { |k, _| [:column].include? k }
+
+ batchable_model = define_batchable_model(table_name, connection: connection)
+
+ scope.call(batchable_model)
+ .each_batch(of: of, **opts) { |batch| yield batch, batchable_model }
end
- def each_batch_range(table_name, connection:, scope: ->(table) { table.all }, of: BATCH_SIZE)
- each_batch(table_name, connection: connection, scope: scope, of: of) do |batch|
- yield batch.pick('MIN(id), MAX(id)')
+ def each_batch_range(table_name, connection:, scope: ->(table) { table.all }, of: BATCH_SIZE, **opts)
+ opts.select! { |k, _| [:column].include? k }
+
+ each_batch(table_name, connection: connection, scope: scope, of: of, **opts) do |batch, batchable_model|
+ column = opts.fetch(:column, batchable_model.primary_key)
+
+ yield batch.pick("MIN(#{column}), MAX(#{column})")
end
end
end
diff --git a/lib/gitlab_settings.rb b/lib/gitlab_settings.rb
index b1f510eaeb1..7f37a125332 100644
--- a/lib/gitlab_settings.rb
+++ b/lib/gitlab_settings.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require "active_support"
-require "active_support/core_ext/hash"
+require 'active_support'
+require 'active_support/core_ext/hash'
require_relative 'gitlab_settings/settings'
require_relative 'gitlab_settings/options'
@@ -10,7 +10,7 @@ module GitlabSettings
MissingSetting = Class.new(StandardError)
def self.load(source = nil, section = nil, &block)
- Settings
+ ::GitlabSettings::Settings
.new(source, section)
.extend(Module.new(&block))
end
diff --git a/lib/gitlab_settings/settings.rb b/lib/gitlab_settings/settings.rb
index 79d006fb118..ae2ed4eac05 100644
--- a/lib/gitlab_settings/settings.rb
+++ b/lib/gitlab_settings/settings.rb
@@ -10,8 +10,7 @@ module GitlabSettings
@source = source
@section = section
-
- reload!
+ @loaded = false
end
def reload!
@@ -19,10 +18,14 @@ module GitlabSettings
all_configs = yaml.deep_stringify_keys
configs = all_configs[section]
- @config = Options.build(configs)
+ @config = Options.build(configs).tap do
+ @loaded = true
+ end
end
def method_missing(name, *args)
+ reload! unless @loaded
+
config.public_send(name, *args) # rubocop: disable GitlabSecurity/PublicSend
end
diff --git a/lib/slack/block_kit/app_home_opened.rb b/lib/slack/block_kit/app_home_opened.rb
new file mode 100644
index 00000000000..f67cdca85d9
--- /dev/null
+++ b/lib/slack/block_kit/app_home_opened.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+# Builds the BlockKit UI JSON payload to respond to the Slack `app_home_opened` event.
+#
+# See:
+# - https://api.slack.com/block-kit/building
+# - https://api.slack.com/events/app_home_opened
+module Slack
+ module BlockKit
+ class AppHomeOpened
+ include ActionView::Helpers::AssetUrlHelper
+ include Gitlab::Routing.url_helpers
+
+ def initialize(slack_user_id, slack_workspace_id, slack_gitlab_user_connection, slack_installation)
+ @slack_user_id = slack_user_id
+ @slack_workspace_id = slack_workspace_id
+ @slack_gitlab_user_connection = slack_gitlab_user_connection
+ @slack_installation = slack_installation
+ end
+
+ def build
+ {
+ type: "home",
+ blocks: [
+ header,
+ section_introduction,
+ section_notifications_heading,
+ section_notifications,
+ section_slash_commands_heading,
+ section_slash_commands,
+ section_slash_commands_connect,
+ section_connect_gitlab_account
+ ]
+ }
+ end
+
+ private
+
+ attr_reader :slack_user_id, :slack_workspace_id, :slack_gitlab_user_connection, :slack_installation
+
+ def header
+ {
+ type: "header",
+ text: {
+ type: "plain_text",
+ text: format(
+ s_("Slack|%{emoji}Welcome to GitLab for Slack!"),
+ emoji: '✨ '
+ ),
+ emoji: true
+ }
+ }
+ end
+
+ def section_introduction
+ section(
+ format(
+ s_("Slack|GitLab for Slack now supports channel-based notifications. " \
+ "Let your team know when new issues are created or new CI/CD jobs are run." \
+ "%{startMarkup}Learn more%{endMarkup}."),
+ startMarkup: " <#{help_page_url('integration/slash_commands')}|",
+ endMarkup: ">"
+ )
+ )
+ end
+
+ def section_notifications_heading
+ section(
+ format(
+ s_("Slack|%{asterisk}Channel notifications%{asterisk}"),
+ asterisk: '*'
+ )
+ )
+ end
+
+ def section_notifications
+ section(
+ format(
+ s_("Slack|To start using notifications, " \
+ "%{startMarkup}enable the GitLab for Slack app integration%{endMarkup} in your project settings."),
+ startMarkup: "<#{help_page_url('user/project/integrations/gitlab_slack_application',
+ anchor: 'configuration')}|",
+ endMarkup: ">"
+ )
+ )
+ end
+
+ def section_slash_commands_heading
+ section(
+ format(
+ s_("Slack|%{asterisk}Slash commands%{asterisk}"),
+ asterisk: '*'
+ )
+ )
+ end
+
+ def section_slash_commands
+ section(
+ format(
+ s_("Slack|Control GitLab from Slack with " \
+ "%{startMarkup}slash commands%{endMarkup}. For a list of available commands, enter %{command}."),
+ startMarkup: "<#{help_page_url('user/project/integrations/gitlab_slack_application', anchor: 'usage')}|",
+ endMarkup: ">",
+ command: "`/gitlab help`"
+ )
+ )
+ end
+
+ def section_slash_commands_connect
+ section(
+ s_("Slack|To start using slash commands, connect your GitLab account.")
+ )
+ end
+
+ def section_connect_gitlab_account
+ if slack_gitlab_user_connection.present?
+ section_gitlab_account_connected
+ else
+ actions_gitlab_account_not_connected
+ end
+ end
+
+ def section_gitlab_account_connected
+ user = slack_gitlab_user_connection.user
+
+ section(
+ format(
+ s_("Slack|%{emoji}Connected to GitLab account %{account}."),
+ emoji: '✅ ',
+ account: "<#{Gitlab::UrlBuilder.build(user)}|#{user.to_reference}>"
+ )
+ )
+ end
+
+ def actions_gitlab_account_not_connected
+ account_connection_url = ChatNames::AuthorizeUserService.new(
+ {
+ team_id: slack_workspace_id,
+ user_id: slack_user_id,
+ team_domain: slack_workspace_id,
+ user_name: 'Slack'
+ }
+ ).execute
+
+ {
+ type: "actions",
+ elements: [
+ {
+ type: "button",
+ text: {
+ type: "plain_text",
+ text: s_("Slack|Connect your GitLab account"),
+ emoji: true
+ },
+ style: "primary",
+ url: account_connection_url
+ }
+ ]
+ }
+ end
+
+ def section(text)
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: text
+ }
+ }
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 96d88983253..76cd90943ae 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1012,9 +1012,6 @@ msgstr ""
msgid "%{reportType} detected no new vulnerabilities."
msgstr ""
-msgid "%{reported} reported for %{category} by %{reporter}"
-msgstr ""
-
msgid "%{requireStart}Require%{requireEnd} %{approvalsRequired} %{approvalStart}approval%{approvalEnd} from:"
msgid_plural "%{requireStart}Require%{requireEnd} %{approvalsRequired} %{approvalStart}approvals%{approvalEnd} from:"
msgstr[0] ""
@@ -2072,6 +2069,12 @@ msgstr ""
msgid "Abuse reports notification email"
msgstr ""
+msgid "AbuseReports|%{reportedUser} reported for %{category} by %{reporter}"
+msgstr ""
+
+msgid "AbuseReports|Deleted user"
+msgstr ""
+
msgid "AbuseReports|No reports found"
msgstr ""
@@ -6911,9 +6914,15 @@ msgstr ""
msgid "BillingPlans|10,000 CI/CD minutes per month"
msgstr ""
+msgid "BillingPlans|10000 CI/CD minutes"
+msgstr ""
+
msgid "BillingPlans|10GB transfer per month"
msgstr ""
+msgid "BillingPlans|400 CI/CD minutes"
+msgstr ""
+
msgid "BillingPlans|400 CI/CD minutes per month"
msgstr ""
@@ -6923,6 +6932,9 @@ msgstr ""
msgid "BillingPlans|50,000 CI/CD minutes per month"
msgstr ""
+msgid "BillingPlans|50000 CI/CD minutes"
+msgstr ""
+
msgid "BillingPlans|5GB storage"
msgstr ""
@@ -6932,9 +6944,21 @@ msgstr ""
msgid "BillingPlans|Advanced CI/CD"
msgstr ""
+msgid "BillingPlans|Advanced application security"
+msgstr ""
+
msgid "BillingPlans|All plans have unlimited (private) repositories."
msgstr ""
+msgid "BillingPlans|All stages of the DevOps lifecycle"
+msgstr ""
+
+msgid "BillingPlans|All the benefits of Free +"
+msgstr ""
+
+msgid "BillingPlans|All the benefits of Premium +"
+msgstr ""
+
msgid "BillingPlans|All the features from Free"
msgstr ""
@@ -6944,15 +6968,30 @@ msgstr ""
msgid "BillingPlans|Billed annually at %{price_per_year} USD"
msgstr ""
+msgid "BillingPlans|Bring your own CI runners"
+msgstr ""
+
+msgid "BillingPlans|Bring your own production environment"
+msgstr ""
+
msgid "BillingPlans|Check out all groups"
msgstr ""
+msgid "BillingPlans|Company wide portfolio management"
+msgstr ""
+
msgid "BillingPlans|Compliance"
msgstr ""
+msgid "BillingPlans|Compliance automation"
+msgstr ""
+
msgid "BillingPlans|Congratulations, your free trial is activated."
msgstr ""
+msgid "BillingPlans|Cross-team project management"
+msgstr ""
+
msgid "BillingPlans|End of availability for the Bronze Plan"
msgstr ""
@@ -6962,6 +7001,9 @@ msgstr ""
msgid "BillingPlans|Enterprise agile planning"
msgstr ""
+msgid "BillingPlans|Executive level insights"
+msgstr ""
+
msgid "BillingPlans|Faster code reviews"
msgstr ""
@@ -6980,6 +7022,9 @@ msgstr ""
msgid "BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}."
msgstr ""
+msgid "BillingPlans|Includes"
+msgstr ""
+
msgid "BillingPlans|Includes free static websites"
msgstr ""
@@ -6998,6 +7043,12 @@ msgstr ""
msgid "BillingPlans|Manage plan"
msgstr ""
+msgid "BillingPlans|Multi-region support"
+msgstr ""
+
+msgid "BillingPlans|Multiple approval rules"
+msgstr ""
+
msgid "BillingPlans|Not the group you're looking for? %{all_groups_link}."
msgstr ""
@@ -7016,6 +7067,9 @@ msgstr ""
msgid "BillingPlans|Pricing page"
msgstr ""
+msgid "BillingPlans|Priority support"
+msgstr ""
+
msgid "BillingPlans|Ready to explore the value of the paid features today? Start a trial, no credit card required."
msgstr ""
@@ -8376,6 +8430,9 @@ msgstr ""
msgid "CICD|Deployment strategy"
msgstr ""
+msgid "CICD|Disabling this feature is a permanent change."
+msgstr ""
+
msgid "CICD|Enable feature to allow job token access by the following projects."
msgstr ""
@@ -8400,6 +8457,9 @@ msgstr ""
msgid "CICD|Select the projects that can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable. It is a security risk to disable this feature, because unauthorized projects might attempt to retrieve an active token and access the API. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
+msgid "CICD|The %{boldStart}Limit CI_JOB_TOKEN%{boldEnd} scope is deprecated and will be removed the 17.0 milestone. Configure the %{boldStart}CI_JOB_TOKEN%{boldEnd} allowlist instead. %{linkStart}How do I do this?%{linkEnd}"
+msgstr ""
+
msgid "CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file. %{link_start}What is Auto DevOps?%{link_end}"
msgstr ""
@@ -29657,6 +29717,9 @@ msgstr ""
msgid "No prioritized labels with such name or description"
msgstr ""
+msgid "No prioritized labels yet!"
+msgstr ""
+
msgid "No project subscribes to the pipelines in this project."
msgstr ""
@@ -31141,15 +31204,15 @@ msgstr ""
msgid "Orphaned member"
msgstr ""
-msgid "Other Labels"
-msgstr ""
-
msgid "Other available runners"
msgstr ""
msgid "Other information"
msgstr ""
+msgid "Other labels"
+msgstr ""
+
msgid "Other merge requests block this MR"
msgstr ""
@@ -32380,7 +32443,7 @@ msgstr ""
msgid "PipelineEditor|The CI/CD configuration is continuously validated. Errors and warnings are displayed when the CI/CD configuration file is not empty."
msgstr ""
-msgid "PipelineEditor|The merged YAML view is displayed when the CI/CD configuration file has valid syntax."
+msgid "PipelineEditor|The full configuration view is displayed when the CI/CD configuration file has valid syntax."
msgstr ""
msgid "PipelineEditor|The pipeline visualization is displayed when the CI/CD configuration file has valid syntax."
@@ -32695,7 +32758,7 @@ msgstr ""
msgid "Pipelines|Could not load artifacts."
msgstr ""
-msgid "Pipelines|Could not load merged YAML content"
+msgid "Pipelines|Could not load full configuration content"
msgstr ""
msgid "Pipelines|Description"
@@ -32713,6 +32776,12 @@ msgstr ""
msgid "Pipelines|Follow these instructions to install GitLab Runner on macOS."
msgstr ""
+msgid "Pipelines|Full configuration"
+msgstr ""
+
+msgid "Pipelines|Full configuration is view only"
+msgstr ""
+
msgid "Pipelines|Get familiar with GitLab CI syntax by setting up a simple pipeline running a \"Hello world\" script to see how it runs, explore how CI/CD works."
msgstr ""
@@ -32755,9 +32824,6 @@ msgstr ""
msgid "Pipelines|Loading pipelines"
msgstr ""
-msgid "Pipelines|Merged YAML is view only"
-msgstr ""
-
msgid "Pipelines|More Information"
msgstr ""
@@ -32878,9 +32944,6 @@ msgstr ""
msgid "Pipelines|Validating GitLab CI configuration…"
msgstr ""
-msgid "Pipelines|View merged YAML"
-msgstr ""
-
msgid "Pipelines|Visualize"
msgstr ""
@@ -33700,10 +33763,10 @@ msgstr ""
msgid "Prioritize label"
msgstr ""
-msgid "Prioritized Labels"
+msgid "Prioritized"
msgstr ""
-msgid "Prioritized label"
+msgid "Prioritized labels"
msgstr ""
msgid "Priority"
@@ -42690,7 +42753,7 @@ msgstr ""
msgid "Standard"
msgstr ""
-msgid "Star labels to start sorting by priority"
+msgid "Star labels to start sorting by priority."
msgstr ""
msgid "Star toggle failed. Try again later."
@@ -48226,9 +48289,6 @@ msgstr ""
msgid "User is not allowed to resolve thread"
msgstr ""
-msgid "User joined %{timeAgo}"
-msgstr ""
-
msgid "User key"
msgstr ""
diff --git a/package.json b/package.json
index d7d9736a056..c3bb53b8f28 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/fonts": "^1.2.0",
"@gitlab/svgs": "3.43.0",
- "@gitlab/ui": "62.5.1",
+ "@gitlab/ui": "62.5.2",
"@gitlab/visual-review-tools": "1.7.3",
"@gitlab/web-ide": "0.0.1-dev-20230425040132",
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
diff --git a/qa/qa/page/project/pipeline_editor/show.rb b/qa/qa/page/project/pipeline_editor/show.rb
index 0a7a4460d18..bf86a0f5edb 100644
--- a/qa/qa/page/project/pipeline_editor/show.rb
+++ b/qa/qa/page/project/pipeline_editor/show.rb
@@ -122,8 +122,8 @@ module QA
go_to_tab('Visualize')
end
- def go_to_view_merged_yaml_tab
- go_to_tab('View merged YAML')
+ def go_to_full_configuration_tab
+ go_to_tab('Full configuration')
end
def go_to_validate_tab
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb
index dbe24e2a2b2..745879cf12f 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb
@@ -74,7 +74,7 @@ module QA
show.simulate_pipeline
expect(show.tab_alert_title).to have_content('Simulation completed successfully')
- show.go_to_view_merged_yaml_tab
+ show.go_to_full_configuration_tab
expect(show).to have_source_editor
end
end
@@ -101,7 +101,7 @@ module QA
expect(show.ci_syntax_validate_message).to have_content('CI configuration is invalid')
- show.go_to_view_merged_yaml_tab
+ show.go_to_full_configuration_tab
# TODO: remove this retry when
# https://gitlab.com/gitlab-org/gitlab/-/issues/378536 is resolved
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index ff24b754d7a..497e2d84f4f 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -38,6 +38,41 @@ RSpec.describe SearchController, feature_category: :global_search do
it_behaves_like 'with external authorization service enabled', :show, { search: 'hello' }
it_behaves_like 'support for active record query timeouts', :show, { search: 'hello' }, :search_objects, :html
+ describe 'rate limit scope' do
+ it 'uses current_user and search scope' do
+ %w[projects blobs users issues merge_requests].each do |scope|
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user, scope])
+ get :show, params: { search: 'hello', scope: scope }
+ end
+ end
+
+ it 'uses just current_user when no search scope is used' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get :show, params: { search: 'hello' }
+ end
+
+ it 'uses just current_user when search scope is abusive' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get(:show, params: { search: 'hello', scope: 'hack-the-mainframe' })
+
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get :show, params: { search: 'hello', scope: 'blobs' * 1000 }
+ end
+
+ context 'when search_rate_limited_scopes feature flag is disabled' do
+ before do
+ stub_feature_flags(search_rate_limited_scopes: false)
+ end
+
+ it 'uses just current_user' do
+ %w[projects blobs users issues merge_requests].each do |scope|
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get :show, params: { search: 'hello', scope: scope }
+ end
+ end
+ end
+ end
+
context 'uses the right partials depending on scope' do
using RSpec::Parameterized::TableSyntax
render_views
@@ -345,6 +380,36 @@ RSpec.describe SearchController, feature_category: :global_search do
expect(json_response).to eq({ 'count' => '1' })
end
+ describe 'rate limit scope' do
+ it 'uses current_user and search scope' do
+ %w[projects blobs users issues merge_requests].each do |scope|
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user, scope])
+ get :count, params: { search: 'hello', scope: scope }
+ end
+ end
+
+ it 'uses just current_user when search scope is abusive' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get :count, params: { search: 'hello', scope: 'hack-the-mainframe' }
+
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get :count, params: { search: 'hello', scope: 'blobs' * 1000 }
+ end
+
+ context 'when search_rate_limited_scopes feature flag is disabled' do
+ before do
+ stub_feature_flags(search_rate_limited_scopes: false)
+ end
+
+ it 'uses just current_user' do
+ %w[projects blobs users issues merge_requests].each do |scope|
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get :count, params: { search: 'hello', scope: scope }
+ end
+ end
+ end
+ end
+
it 'raises an error if search term is missing' do
expect do
get :count, params: { scope: 'projects' }
@@ -406,6 +471,36 @@ RSpec.describe SearchController, feature_category: :global_search do
expect(json_response).to match_array([])
end
+ describe 'rate limit scope' do
+ it 'uses current_user and search scope' do
+ %w[projects blobs users issues merge_requests].each do |scope|
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user, scope])
+ get :autocomplete, params: { term: 'hello', scope: scope }
+ end
+ end
+
+ it 'uses just current_user when search scope is abusive' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get :autocomplete, params: { term: 'hello', scope: 'hack-the-mainframe' }
+
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get :autocomplete, params: { term: 'hello', scope: 'blobs' * 1000 }
+ end
+
+ context 'when search_rate_limited_scopes feature flag is disabled' do
+ before do
+ stub_feature_flags(search_rate_limited_scopes: false)
+ end
+
+ it 'uses just current_user' do
+ %w[projects blobs users issues merge_requests].each do |scope|
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit, scope: [user])
+ get :autocomplete, params: { term: 'hello', scope: scope }
+ end
+ end
+ end
+ end
+
it_behaves_like 'rate limited endpoint', rate_limit_key: :search_rate_limit do
let(:current_user) { user }
@@ -525,6 +620,11 @@ RSpec.describe SearchController, feature_category: :global_search do
get endpoint, params: params.merge(project_id: project.id)
end
end
+
+ it 'uses request IP as rate limiting scope' do
+ expect(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).with(:search_rate_limit_unauthenticated, scope: [request.ip])
+ get endpoint, params: params.merge(project_id: project.id)
+ end
end
end
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 0620221051e..9fe72b981f1 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -91,51 +91,6 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
expect(report_rows[1].text).to include(report_text(open_report2))
end
- it 'can be actioned on' do
- open_actions_dropdown(report_rows[0])
-
- expect(page).to have_content('Remove user & report')
- expect(page).to have_content('Block user')
- expect(page).to have_content('Remove report')
-
- # Remove a report
- click_button('Remove report')
- wait_for_requests
-
- expect_displayed_reports_count(1)
- expect_report_shown(open_report)
-
- # Block reported user
- open_actions_dropdown(report_rows[0])
-
- click_button('Block user')
- expect(page).to have_content('USER WILL BE BLOCKED! Are you sure?')
-
- click_button('OK')
- wait_for_requests
-
- expect(page).to have_content('Successfully blocked')
- expect(open_report.user.reload.blocked?).to eq true
-
- open_actions_dropdown(report_rows[0])
-
- expect(page).to have_content('Already blocked')
- expect(page).not_to have_content('Block user')
-
- # Remove user & report
- click_button('Remove user & report')
- expect(page).to have_content("USER #{open_report.user.name} WILL BE REMOVED! Are you sure?")
-
- click_button('OK')
- expect_displayed_reports_count(0)
- end
-
- def open_actions_dropdown(report_row)
- within(report_row) do
- find('[data-testid="base-dropdown-toggle"]').click
- end
- end
-
def report_rows
page.all(abuse_report_row_selector)
end
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index b527b8926a0..4af5dd380c1 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -101,7 +101,7 @@ RSpec.describe 'Prioritize labels', feature_category: :team_planning do
expect(page).to have_content 'wontfix'
# Sort labels
- drag_to(selector: '.label-list-item', from_index: 1, to_index: 2)
+ drag_to(selector: '.label-list-item .label-content', from_index: 1, to_index: 2)
page.within('.prioritized-labels') do
expect(first('.label-list-item')).to have_content('feature')
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_details_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_details_spec.js
deleted file mode 100644
index b89bbac0196..00000000000
--- a/spec/frontend/admin/abuse_reports/components/abuse_report_details_spec.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { GlButton, GlCollapse } from '@gitlab/ui';
-import { nextTick } from 'vue';
-import { shallowMount } from '@vue/test-utils';
-import AbuseReportDetails from '~/admin/abuse_reports/components/abuse_report_details.vue';
-import { getTimeago } from '~/lib/utils/datetime_utility';
-import { mockAbuseReports } from '../mock_data';
-
-describe('AbuseReportDetails', () => {
- let wrapper;
- const report = mockAbuseReports[0];
-
- const findToggleButton = () => wrapper.findComponent(GlButton);
- const findCollapsible = () => wrapper.findComponent(GlCollapse);
-
- const createComponent = () => {
- wrapper = shallowMount(AbuseReportDetails, {
- propsData: {
- report,
- },
- });
- };
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders toggle button with the correct text', () => {
- expect(findToggleButton().text()).toEqual('Show details');
- });
-
- it('renders collapsed GlCollapse containing the report details', () => {
- const collapsible = findCollapsible();
- expect(collapsible.attributes('visible')).toBeUndefined();
-
- const userJoinedText = `User joined ${getTimeago().format(report.reportedUser.createdAt)}`;
- expect(collapsible.text()).toMatch(userJoinedText);
- expect(collapsible.text()).toMatch(report.message);
- });
- });
-
- describe('when toggled', () => {
- it('expands GlCollapse and updates toggle text', async () => {
- createComponent();
-
- findToggleButton().vm.$emit('click');
- await nextTick();
-
- expect(findToggleButton().text()).toEqual('Hide details');
- expect(findCollapsible().attributes('visible')).toBe('true');
- });
- });
-});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
index 9876ee70e5e..f3cced81478 100644
--- a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
@@ -1,9 +1,6 @@
-import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
-import AbuseReportDetails from '~/admin/abuse_reports/components/abuse_report_details.vue';
import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue';
-import AbuseReportActions from '~/admin/abuse_reports/components/abuse_report_actions.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
import { SORT_UPDATED_AT } from '~/admin/abuse_reports/constants';
@@ -13,19 +10,16 @@ describe('AbuseReportRow', () => {
let wrapper;
const mockAbuseReport = mockAbuseReports[0];
- const findLinks = () => wrapper.findAllComponents(GlLink);
- const findAbuseReportActions = () => wrapper.findComponent(AbuseReportActions);
const findListItem = () => wrapper.findComponent(ListItem);
const findTitle = () => wrapper.findByTestId('title');
const findDisplayedDate = () => wrapper.findByTestId('abuse-report-date');
- const findAbuseReportDetails = () => wrapper.findComponent(AbuseReportDetails);
- const createComponent = () => {
+ const createComponent = (props = {}) => {
wrapper = shallowMountExtended(AbuseReportRow, {
propsData: {
report: mockAbuseReport,
+ ...props,
},
- stubs: { GlSprintf },
});
};
@@ -37,19 +31,42 @@ describe('AbuseReportRow', () => {
expect(findListItem().exists()).toBe(true);
});
- it('displays correctly formatted title', () => {
- const { reporter, reportedUser, category, reportedUserPath, reporterPath } = mockAbuseReport;
- expect(findTitle().text()).toMatchInterpolatedText(
- `${reportedUser.name} reported for ${category} by ${reporter.name}`,
- );
+ describe('title', () => {
+ const { reporter, reportedUser, category, reportPath } = mockAbuseReport;
- const userLink = findLinks().at(0);
- expect(userLink.text()).toEqual(reportedUser.name);
- expect(userLink.attributes('href')).toEqual(reportedUserPath);
+ it('displays correctly formatted title', () => {
+ expect(findTitle().text()).toMatchInterpolatedText(
+ `${reportedUser.name} reported for ${category} by ${reporter.name}`,
+ );
+ });
+
+ it('links to the details page', () => {
+ expect(findTitle().attributes('href')).toEqual(reportPath);
+ });
+
+ describe('when the reportedUser is missing', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...mockAbuseReport, reportedUser: null } });
+ });
+
+ it('displays correctly formatted title', () => {
+ expect(findTitle().text()).toMatchInterpolatedText(
+ `Deleted user reported for ${category} by ${reporter.name}`,
+ );
+ });
+ });
- const reporterLink = findLinks().at(1);
- expect(reporterLink.text()).toEqual(reporter.name);
- expect(reporterLink.attributes('href')).toEqual(reporterPath);
+ describe('when the reporter is missing', () => {
+ beforeEach(() => {
+ createComponent({ report: { ...mockAbuseReport, reporter: null } });
+ });
+
+ it('displays correctly formatted title', () => {
+ expect(findTitle().text()).toMatchInterpolatedText(
+ `${reportedUser.name} reported for ${category} by Deleted user`,
+ );
+ });
+ });
});
describe('displayed date', () => {
@@ -71,16 +88,4 @@ describe('AbuseReportRow', () => {
});
});
});
-
- it('renders AbuseReportDetails', () => {
- expect(findAbuseReportDetails().exists()).toBe(true);
- expect(findAbuseReportDetails().props('report')).toEqual(mockAbuseReport);
- });
-
- it('renders AbuseReportRowActions with the correct props', () => {
- const actions = findAbuseReportActions();
-
- expect(actions.exists()).toBe(true);
- expect(actions.props('report')).toMatchObject(mockAbuseReport);
- });
});
diff --git a/spec/frontend/admin/abuse_reports/mock_data.js b/spec/frontend/admin/abuse_reports/mock_data.js
index ee9e56d043b..1ea6ea7d131 100644
--- a/spec/frontend/admin/abuse_reports/mock_data.js
+++ b/spec/frontend/admin/abuse_reports/mock_data.js
@@ -4,14 +4,7 @@ export const mockAbuseReports = [
createdAt: '2018-10-03T05:46:38.977Z',
updatedAt: '2022-12-07T06:45:39.977Z',
reporter: { name: 'Ms. Admin' },
- reportedUser: { name: 'Mr. Abuser', createdAt: '2017-09-01T05:46:38.977Z' },
- reportedUserPath: '/mr_abuser',
- reporterPath: '/admin',
- userBlocked: false,
- blockUserPath: '/block/user/mr_abuser/path',
- removeUserAndReportPath: '/remove/user/mr_abuser/and/report/path',
- removeReportPath: '/remove/report/path',
- message: 'message 1',
+ reportedUser: { name: 'Mr. Abuser' },
reportPath: '/admin/abuse_reports/1',
},
{
@@ -19,14 +12,7 @@ export const mockAbuseReports = [
createdAt: '2018-10-03T05:46:38.977Z',
updatedAt: '2022-12-07T06:45:39.977Z',
reporter: { name: 'Ms. Reporter' },
- reportedUser: { name: 'Mr. Phisher', createdAt: '2016-09-01T05:46:38.977Z' },
- reportedUserPath: '/mr_phisher',
- reporterPath: '/admin',
- userBlocked: false,
- blockUserPath: '/block/user/mr_phisher/path',
- removeUserAndReportPath: '/remove/user/mr_phisher/and/report/path',
- removeReportPath: '/remove/report/path',
- message: 'message 2',
+ reportedUser: { name: 'Mr. Phisher' },
reportPath: '/admin/abuse_reports/2',
},
];
diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
index f8be035d33c..8834231aaef 100644
--- a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
@@ -4,6 +4,7 @@ import { mount } from '@vue/test-utils';
import Vue from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { mockTracking } from 'helpers/tracking_helper';
import CommitForm from '~/ci/pipeline_editor/components/commit/commit_form.vue';
import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
import {
@@ -11,12 +12,12 @@ import {
COMMIT_ACTION_UPDATE,
COMMIT_SUCCESS,
COMMIT_SUCCESS_WITH_REDIRECT,
+ pipelineEditorTrackingOptions,
} from '~/ci/pipeline_editor/constants';
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import commitCreate from '~/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
import updatePipelineEtag from '~/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
-
import {
mockCiConfigPath,
mockCiYml,
@@ -280,4 +281,43 @@ describe('Pipeline Editor | Commit section', () => {
createComponent({ props: { 'scroll-to-commit-form': true } });
expect(findCommitForm().props('scrollToCommitForm')).toBe(true);
});
+
+ describe('tracking', () => {
+ let trackingSpy;
+ const { actions, label } = pipelineEditorTrackingOptions;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ describe('when user commit a new file', () => {
+ beforeEach(async () => {
+ mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
+ createComponentWithApollo({ props: { isNewCiConfigFile: true } });
+ await submitCommit();
+ });
+
+ it('calls tracking event with the CREATE property', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.commitCiConfig, {
+ label,
+ property: COMMIT_ACTION_CREATE,
+ });
+ });
+ });
+
+ describe('when user commit an update to the CI file', () => {
+ beforeEach(async () => {
+ mockMutateCommitData.mockResolvedValue(mockCommitCreateResponse);
+ createComponentWithApollo({ props: { isNewCiConfigFile: false } });
+ await submitCommit();
+ });
+
+ it('calls the tracking event with the UPDATE property', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, actions.commitCiConfig, {
+ label,
+ property: COMMIT_ACTION_UPDATE,
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/jobs/components/table/job_table_app_spec.js b/spec/frontend/jobs/components/table/job_table_app_spec.js
index 1ca28fefd28..30f674f5ba7 100644
--- a/spec/frontend/jobs/components/table/job_table_app_spec.js
+++ b/spec/frontend/jobs/components/table/job_table_app_spec.js
@@ -117,6 +117,16 @@ describe('Job table app', () => {
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
});
+ it('avoids refetch jobs query when scope has not changed', async () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findTabs().vm.$emit('fetchJobsByStatus', null);
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ });
+
describe('when infinite scrolling is triggered', () => {
it('does not display a skeleton loader', () => {
triggerInfiniteScroll();
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index 8f92ab46714..3b8c9dd3bf3 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -384,12 +384,12 @@ describe('MergeRequestTabs', () => {
});
});
- it('scrolls to 0, if no position is stored', () => {
+ it('does not scroll if no position is stored', () => {
testContext.class.tabShown('unknownTab');
jest.advanceTimersByTime(250);
- expect(window.scrollTo.mock.calls[0][0]).toEqual({ top: 0, left: 0, behavior: 'auto' });
+ expect(window.scrollTo).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
index 44a5878e6f2..cc6f1c27142 100644
--- a/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/table/admin_job_table_app_spec.js
@@ -129,6 +129,16 @@ describe('Job table app', () => {
expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(1);
});
+ it('avoids refetch jobs query when scope has not changed', async () => {
+ jest.spyOn(wrapper.vm.$apollo.queries.jobs, 'refetch').mockImplementation(jest.fn());
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+
+ await findTabs().vm.$emit('fetchJobsByStatus', null);
+
+ expect(wrapper.vm.$apollo.queries.jobs.refetch).toHaveBeenCalledTimes(0);
+ });
+
describe('when infinite scrolling is triggered', () => {
it('does not display a skeleton loader', () => {
triggerInfiniteScroll();
diff --git a/spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js b/spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js
new file mode 100644
index 00000000000..3366d60d9f3
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/components/table/cells/project_cell_spec.js
@@ -0,0 +1,32 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ProjectCell from '~/pages/admin/jobs/components/table/cell/project_cell.vue';
+import { mockAllJobsNodes } from '../../../../../../jobs/mock_data';
+
+const mockJob = mockAllJobsNodes[0];
+
+describe('Project cell', () => {
+ let wrapper;
+
+ const findProjectLink = () => wrapper.findComponent(GlLink);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ProjectCell, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('Project Link', () => {
+ beforeEach(() => {
+ createComponent({ job: mockJob });
+ });
+
+ it('shows and links to the project', () => {
+ expect(findProjectLink().exists()).toBe(true);
+ expect(findProjectLink().text()).toBe(mockJob.pipeline.project.fullPath);
+ expect(findProjectLink().attributes('href')).toBe(mockJob.pipeline.project.webUrl);
+ });
+ });
+});
diff --git a/spec/frontend/token_access/outbound_token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js
index 347ea1178bc..7f321495d72 100644
--- a/spec/frontend/token_access/outbound_token_access_spec.js
+++ b/spec/frontend/token_access/outbound_token_access_spec.js
@@ -44,15 +44,26 @@ describe('TokenAccess component', () => {
const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' });
const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
const findTokenDisabledAlert = () => wrapper.findByTestId('token-disabled-alert');
+ const findDeprecationAlert = () => wrapper.findByTestId('deprecation-alert');
+ const findProjectPathInput = () => wrapper.findByTestId('project-path-input');
const createMockApolloProvider = (requestHandlers) => {
return createMockApollo(requestHandlers);
};
- const createComponent = (requestHandlers, mountFn = shallowMountExtended) => {
+ const createComponent = (
+ requestHandlers,
+ mountFn = shallowMountExtended,
+ frozenOutboundJobTokenScopes = false,
+ frozenOutboundJobTokenScopesOverride = false,
+ ) => {
wrapper = mountFn(OutboundTokenAccess, {
provide: {
fullPath: projectPath,
+ glFeatures: {
+ frozenOutboundJobTokenScopes,
+ frozenOutboundJobTokenScopesOverride,
+ },
},
apolloProvider: createMockApolloProvider(requestHandlers),
data() {
@@ -272,4 +283,59 @@ describe('TokenAccess component', () => {
expect(createAlert).toHaveBeenCalledWith({ message });
});
});
+
+ describe('with the frozenOutboundJobTokenScopes feature flag enabled', () => {
+ describe('toggle', () => {
+ it('the toggle is off and the deprecation alert is visible', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ],
+ shallowMountExtended,
+ true,
+ );
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(findToggle().props('disabled')).toBe(true);
+ expect(findDeprecationAlert().exists()).toBe(true);
+ expect(findTokenDisabledAlert().exists()).toBe(false);
+ });
+
+ it('contains a warning message about disabling the current configuration', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ],
+ mountExtended,
+ true,
+ );
+
+ await waitForPromises();
+
+ expect(findToggle().text()).toContain('Disabling this feature is a permanent change.');
+ });
+ });
+
+ describe('adding a new project', () => {
+ it('disables the input to add new projects', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ],
+ mountExtended,
+ true,
+ false,
+ );
+
+ await waitForPromises();
+
+ expect(findProjectPathInput().attributes('disabled')).toBe('disabled');
+ });
+ });
+ });
});
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 6eb97a99264..b7fdadbd036 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe AvatarsHelper do
+RSpec.describe AvatarsHelper, feature_category: :source_code_management do
include UploadHelpers
let_it_be(:user) { create(:user) }
@@ -88,7 +88,7 @@ RSpec.describe AvatarsHelper do
describe '#avatar_icon_for' do
let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') }
let(:email) { 'foo@example.com' }
- let!(:another_user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: email) }
+ let!(:another_user) { create(:user, :public_email, avatar: File.open(uploaded_image_temp_path), email: email) }
it 'prefers the user to retrieve the avatar_url' do
expect(helper.avatar_icon_for(user, email).to_s)
@@ -102,7 +102,7 @@ RSpec.describe AvatarsHelper do
end
describe '#avatar_icon_for_email', :clean_gitlab_redis_cache do
- let(:user) { create(:user, avatar: File.open(uploaded_image_temp_path)) }
+ let(:user) { create(:user, :public_email, avatar: File.open(uploaded_image_temp_path)) }
subject { helper.avatar_icon_for_email(user.email).to_s }
@@ -114,6 +114,14 @@ RSpec.describe AvatarsHelper do
end
end
+ context 'when a private email is used' do
+ it 'calls gravatar_icon' do
+ expect(helper).to receive(:gravatar_icon).with(user.commit_email, 20, 2)
+
+ helper.avatar_icon_for_email(user.commit_email, 20, 2)
+ end
+ end
+
context 'when no user exists for the email' do
it 'calls gravatar_icon' do
expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
@@ -136,7 +144,7 @@ RSpec.describe AvatarsHelper do
it_behaves_like "returns avatar for email"
it "caches the request" do
- expect(User).to receive(:find_by_any_email).once.and_call_original
+ expect(User).to receive(:with_public_email).once.and_call_original
expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
expect(helper.avatar_icon_for_email(user.email).to_s).to eq(user.avatar.url)
diff --git a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
index 004c70c28f1..dc6ac52a8c2 100644
--- a/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
+++ b/spec/lib/banzai/filter/asset_proxy_filter_spec.rb
@@ -80,6 +80,15 @@ RSpec.describe Banzai::Filter::AssetProxyFilter, feature_category: :team_plannin
expect(doc.at_css('img')['data-canonical-src']).to eq src
end
+ it 'replaces invalid URLs' do
+ src = '///example.com/test.png'
+ new_src = 'https://assets.example.com/3368d2c7b9bed775bdd1e811f36a4b80a0dcd8ab/2f2f2f6578616d706c652e636f6d2f746573742e706e67'
+ doc = filter(image(src), @context)
+
+ expect(doc.at_css('img')['src']).to eq new_src
+ expect(doc.at_css('img')['data-canonical-src']).to eq src
+ end
+
it 'skips internal images' do
src = "#{Gitlab.config.gitlab.url}/test.png"
doc = filter(image(src), @context)
diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
index 3ebe0798972..3d992f962ec 100644
--- a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
@@ -8,16 +8,15 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c
include CommitTrailersSpecHelper
let(:secondary_email) { create(:email, :confirmed) }
- let(:user) { create(:user) }
+ let(:user) { create(:user, :public_email) }
+ let(:email) { FFaker::Internet.email }
let(:trailer) { "#{FFaker::Lorem.word}-by:" }
- let(:commit_message) { trailer_line(trailer, user.name, user.email) }
+ let(:commit_message) { trailer_line(trailer, user.name, user.public_email) }
let(:commit_message_html) { commit_html(commit_message) }
context 'detects' do
- let(:email) { FFaker::Internet.email }
-
context 'trailers in the form of *-by' do
where(:commit_trailer) do
["#{FFaker::Lorem.word}-by:", "#{FFaker::Lorem.word}-BY:", "#{FFaker::Lorem.word}-By:"]
@@ -42,7 +41,7 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c
expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
end
- it 'GitLab users via a secondary email' do
+ it 'does not detect GitLab users via a secondary email' do
_, message_html = build_commit_message(
trailer: trailer,
name: secondary_email.user.name,
@@ -51,9 +50,8 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c
doc = filter(message_html)
- expect_to_have_user_link_with_avatar(
+ expect_to_have_mailto_link_with_avatar(
doc,
- user: secondary_email.user,
trailer: trailer,
email: secondary_email.email
)
@@ -185,17 +183,16 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c
it 'preserves the original email used in the commit message' do
message, message_html = build_commit_message(
trailer: trailer,
- name: secondary_email.user.name,
- email: secondary_email.email
+ name: user.name,
+ email: email
)
doc = filter(message_html)
- expect_to_have_user_link_with_avatar(
+ expect_to_have_mailto_link_with_avatar(
doc,
- user: secondary_email.user,
trailer: trailer,
- email: secondary_email.email
+ email: email
)
expect(doc.text).to match Regexp.escape(message)
end
@@ -218,7 +215,7 @@ RSpec.describe Banzai::Filter::CommitTrailersFilter, feature_category: :source_c
# any path-only link will automatically be prefixed
# with the path of its repository.
# See: "build_relative_path" in "lib/banzai/filter/relative_link_filter.rb"
- let(:user_with_avatar) { create(:user, :with_avatar, username: 'foobar') }
+ let(:user_with_avatar) { create(:user, :public_email, :with_avatar, username: 'foobar') }
it 'returns a full path for avatar urls' do
_, message_html = build_commit_message(
diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb
index d6280d3c28c..7f535e86d69 100644
--- a/spec/lib/gitlab/checks/branch_check_spec.rb
+++ b/spec/lib/gitlab/checks/branch_check_spec.rb
@@ -26,8 +26,14 @@ RSpec.describe Gitlab::Checks::BranchCheck do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
end
+ it "prohibits 40-character hexadecimal branch names as the start of a path" do
+ allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e/test")
+
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "You cannot create a branch with a 40-character hexadecimal branch name.")
+ end
+
it "doesn't prohibit a nested hexadecimal in a branch name" do
- allow(subject).to receive(:branch_name).and_return("fix-267208abfe40e546f5e847444276f7d43a39503e")
+ allow(subject).to receive(:branch_name).and_return("267208abfe40e546f5e847444276f7d43a39503e-fix")
expect { subject.validate! }.not_to raise_error
end
diff --git a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
index 31486240bfa..fe423b3639b 100644
--- a/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
+++ b/spec/lib/gitlab/database/dynamic_model_helpers_spec.rb
@@ -49,6 +49,21 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers do
expect { |b| each_batch_size.call(&b) }
.to yield_successive_args(1, 1)
end
+
+ context 'when a column to be batched over is specified' do
+ let(:projects) { Project.order(project_namespace_id: :asc) }
+
+ it 'iterates table in batches using the given column' do
+ each_batch_ids = ->(&block) do
+ subject.each_batch(table_name, connection: connection, of: 1, column: :project_namespace_id) do |batch|
+ block.call(batch.pluck(:project_namespace_id))
+ end
+ end
+
+ expect { |b| each_batch_ids.call(&b) }
+ .to yield_successive_args([projects.first.project_namespace_id], [projects.last.project_namespace_id])
+ end
+ end
end
context 'when transaction is open' do
@@ -95,6 +110,35 @@ RSpec.describe Gitlab::Database::DynamicModelHelpers do
expect { |b| each_batch_limited.call(&b) }
.to yield_successive_args([first_project.id, first_project.id])
end
+
+ context 'when primary key is not named id' do
+ let(:namespace_settings1) { create(:namespace_settings) }
+ let(:namespace_settings2) { create(:namespace_settings) }
+ let(:table_name) { NamespaceSetting.table_name }
+ let(:connection) { NamespaceSetting.connection }
+ let(:primary_key) { subject.define_batchable_model(table_name, connection: connection).primary_key }
+
+ it 'iterates table in batch ranges using the correct primary key' do
+ expect(primary_key).to eq("namespace_id") # Sanity check the primary key is not id
+ expect { |b| subject.each_batch_range(table_name, connection: connection, of: 1, &b) }
+ .to yield_successive_args(
+ [namespace_settings1.namespace_id, namespace_settings1.namespace_id],
+ [namespace_settings2.namespace_id, namespace_settings2.namespace_id]
+ )
+ end
+ end
+
+ context 'when a column to be batched over is specified' do
+ it 'iterates table in batch ranges using the given column' do
+ expect do |b|
+ subject.each_batch_range(table_name, connection: connection, of: 1, column: :project_namespace_id, &b)
+ end
+ .to yield_successive_args(
+ [first_project.project_namespace_id, first_project.project_namespace_id],
+ [second_project.project_namespace_id, second_project.project_namespace_id]
+ )
+ end
+ end
end
context 'when transaction is open' do
diff --git a/spec/lib/gitlab_settings/settings_spec.rb b/spec/lib/gitlab_settings/settings_spec.rb
index 55ceff4ce82..161c26dbb9f 100644
--- a/spec/lib/gitlab_settings/settings_spec.rb
+++ b/spec/lib/gitlab_settings/settings_spec.rb
@@ -21,18 +21,16 @@ RSpec.describe GitlabSettings::Settings, :aggregate_failures, feature_category:
subject(:settings) { described_class.new(source.path, 'section1') }
- it 'requires a source' do
- expect { described_class.new('', '') }
- .to raise_error(ArgumentError, 'config source is required')
- end
-
- it 'requires a section' do
- expect { described_class.new(source, '') }
- .to raise_error(ArgumentError, 'config section is required')
- end
+ describe '#initialize' do
+ it 'requires a source' do
+ expect { described_class.new('', '') }
+ .to raise_error(ArgumentError, 'config source is required')
+ end
- it 'loads the given section config' do
- expect(settings.config1.value1).to eq(1)
+ it 'requires a section' do
+ expect { described_class.new(source, '') }
+ .to raise_error(ArgumentError, 'config section is required')
+ end
end
describe '#reload!' do
@@ -50,4 +48,20 @@ RSpec.describe GitlabSettings::Settings, :aggregate_failures, feature_category:
expect(settings.config1.value1).to eq(2)
end
end
+
+ it 'loads the given section config' do
+ expect(settings.config1.value1).to eq(1)
+ end
+
+ context 'on lazy loading' do
+ it 'does not raise exception on initialization if source does not exists' do
+ settings = nil
+
+ expect { settings = described_class.new('/tmp/any/inexisting/file.yml', 'section1') }
+ .not_to raise_error
+
+ expect { settings['any key'] }
+ .to raise_error(Errno::ENOENT)
+ end
+ end
end
diff --git a/spec/lib/slack/block_kit/app_home_opened_spec.rb b/spec/lib/slack/block_kit/app_home_opened_spec.rb
new file mode 100644
index 00000000000..5a5a9c6739c
--- /dev/null
+++ b/spec/lib/slack/block_kit/app_home_opened_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Slack::BlockKit::AppHomeOpened, feature_category: :integrations do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:chat_name) { nil }
+
+ describe '#build' do
+ subject(:payload) do
+ described_class.new(slack_installation.user_id, slack_installation.team_id, chat_name, slack_installation).build
+ end
+
+ it 'generates blocks of type "home"' do
+ is_expected.to match({ type: 'home', blocks: kind_of(Array) })
+ end
+
+ it 'prompts the user to connect their GitLab account' do
+ expect(payload[:blocks]).to include(
+ hash_including(
+ {
+ type: 'actions',
+ elements: [
+ hash_including(
+ {
+ type: 'button',
+ text: include({ text: 'Connect your GitLab account' }),
+ url: include(Gitlab::Routing.url_helpers.new_profile_chat_name_url)
+ }
+ )
+ ]
+ }
+ )
+ )
+ end
+
+ context 'when the user has linked their GitLab account' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:chat_name) do
+ create(:chat_name,
+ user: user,
+ team_id: slack_installation.team_id,
+ chat_id: slack_installation.user_id
+ )
+ end
+
+ it 'displays the GitLab user they are linked to' do
+ account = "<#{Gitlab::UrlBuilder.build(user)}|#{user.to_reference}>"
+
+ expect(payload[:blocks]).to include(
+ hash_including(
+ {
+ type: 'section',
+ text: include({ text: "✅ Connected to GitLab account #{account}." })
+ }
+ )
+ )
+ end
+ end
+ end
+end
diff --git a/spec/migrations/swap_system_note_metadata_note_id_to_bigint_for_gitlab_dot_com_spec.rb b/spec/migrations/swap_system_note_metadata_note_id_to_bigint_for_gitlab_dot_com_spec.rb
new file mode 100644
index 00000000000..4fa5814986a
--- /dev/null
+++ b/spec/migrations/swap_system_note_metadata_note_id_to_bigint_for_gitlab_dot_com_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapSystemNoteMetadataNoteIdToBigintForGitlabDotCom, feature_category: :database do
+ describe '#up' do
+ before do
+ # A we call `schema_migrate_down!` before each example, and for this migration
+ # `#down` is same as `#up`, we need to ensure we start from the expected state.
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE system_note_metadata ALTER COLUMN note_id TYPE integer')
+ connection.execute('ALTER TABLE system_note_metadata ALTER COLUMN note_id_convert_to_bigint TYPE bigint')
+ end
+
+ # rubocop: disable RSpec/AnyInstanceOf
+ it 'swaps the integer and bigint columns for GitLab.com, dev, or test' do
+ allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(true)
+
+ metadata = table(:system_note_metadata)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ metadata.reset_column_information
+
+ expect(metadata.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('integer')
+ expect(metadata.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ metadata.reset_column_information
+
+ expect(metadata.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('bigint')
+ expect(metadata.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('integer')
+ }
+ end
+ end
+ end
+
+ it 'is a no-op for other instances' do
+ allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
+
+ metadata = table(:system_note_metadata)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ metadata.reset_column_information
+
+ expect(metadata.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('integer')
+ expect(metadata.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ metadata.reset_column_information
+
+ expect(metadata.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('integer')
+ expect(metadata.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+ end
+ end
+ end
+ # rubocop: enable RSpec/AnyInstanceOf
+ end
+end
diff --git a/spec/migrations/swap_todos_note_id_to_bigint_for_gitlab_dot_com_spec.rb b/spec/migrations/swap_todos_note_id_to_bigint_for_gitlab_dot_com_spec.rb
new file mode 100644
index 00000000000..e71c921998a
--- /dev/null
+++ b/spec/migrations/swap_todos_note_id_to_bigint_for_gitlab_dot_com_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe SwapTodosNoteIdToBigintForGitlabDotCom, feature_category: :database do
+ describe '#up' do
+ before do
+ # A we call `schema_migrate_down!` before each example, and for this migration
+ # `#down` is same as `#up`, we need to ensure we start from the expected state.
+ connection = described_class.new.connection
+ connection.execute('ALTER TABLE todos ALTER COLUMN note_id TYPE integer')
+ connection.execute('ALTER TABLE todos ALTER COLUMN note_id_convert_to_bigint TYPE bigint')
+ end
+
+ # rubocop: disable RSpec/AnyInstanceOf
+ it 'swaps the integer and bigint columns for GitLab.com, dev, or test' do
+ allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(true)
+
+ todos = table(:todos)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ todos.reset_column_information
+
+ expect(todos.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('integer')
+ expect(todos.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ todos.reset_column_information
+
+ expect(todos.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('bigint')
+ expect(todos.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('integer')
+ }
+ end
+ end
+ end
+
+ it 'is a no-op for other instances' do
+ allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
+
+ todos = table(:todos)
+
+ disable_migrations_output do
+ reversible_migration do |migration|
+ migration.before -> {
+ todos.reset_column_information
+
+ expect(todos.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('integer')
+ expect(todos.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+
+ migration.after -> {
+ todos.reset_column_information
+
+ expect(todos.columns.find { |c| c.name == 'note_id' }.sql_type).to eq('integer')
+ expect(todos.columns.find { |c| c.name == 'note_id_convert_to_bigint' }.sql_type).to eq('bigint')
+ }
+ end
+ end
+ end
+ # rubocop: enable RSpec/AnyInstanceOf
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index c994aca2991..7b1907f2a6a 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -4259,10 +4259,14 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
subject { create(:merge_request, source_project: project) }
- it 'fetches the ref correctly' do
+ it 'fetches the ref and expires the ancestor cache' do
expect { subject.target_project.repository.delete_refs(subject.ref_path) }.not_to raise_error
+ expect(project.repository).to receive(:expire_ancestor_cache).with(subject.target_branch_sha, subject.diff_head_sha).and_call_original
+ expect(subject).to receive(:expire_ancestor_cache).and_call_original
+
subject.fetch_ref!
+
expect(subject.target_project.repository.ref_exists?(subject.ref_path)).to be_truthy
end
end
@@ -4273,7 +4277,8 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
# We use build instead of create to test that an IID is allocated
subject { build(:merge_request, source_project: project) }
- it 'fetches the ref correctly' do
+ it 'fetches the ref and expires the ancestor cache' do
+ expect(subject).to receive(:expire_ancestor_cache).and_call_original
expect(subject.iid).to be_nil
expect { subject.eager_fetch_ref! }.to change { subject.iid.to_i }.by(1)
diff --git a/spec/models/namespace/package_setting_spec.rb b/spec/models/namespace/package_setting_spec.rb
index 2584fa597ad..fca929600a4 100644
--- a/spec/models/namespace/package_setting_spec.rb
+++ b/spec/models/namespace/package_setting_spec.rb
@@ -47,28 +47,30 @@ RSpec.describe Namespace::PackageSetting do
context 'package types with package_settings' do
# As more package types gain settings they will be added to this list
[:maven_package, :generic_package].each do |format|
- let_it_be(:package) { create(format, name: 'foo', version: 'beta') } # rubocop:disable Rails/SaveBang
- let_it_be(:package_type) { package.package_type }
- let_it_be(:package_setting) { package.project.namespace.package_settings }
-
- where(:duplicates_allowed, :duplicate_exception_regex, :result) do
- true | '' | true
- false | '' | false
- false | '.*' | true
- false | 'fo.*' | true
- false | 'be.*' | true
- end
+ context "with package_type:#{format}" do
+ let_it_be(:package) { create(format, name: 'foo', version: 'beta') } # rubocop:disable Rails/SaveBang
+ let_it_be(:package_type) { package.package_type }
+ let_it_be(:package_setting) { package.project.namespace.package_settings }
+
+ where(:duplicates_allowed, :duplicate_exception_regex, :result) do
+ true | '' | true
+ false | '' | false
+ false | '.*' | true
+ false | 'fo.*' | true
+ false | 'be.*' | true
+ end
- with_them do
- context "for #{format}" do
- before do
- package_setting.update!(
- "#{package_type}_duplicates_allowed" => duplicates_allowed,
- "#{package_type}_duplicate_exception_regex" => duplicate_exception_regex
- )
- end
+ with_them do
+ context "for #{format}" do
+ before do
+ package_setting.update!(
+ "#{package_type}_duplicates_allowed" => duplicates_allowed,
+ "#{package_type}_duplicate_exception_regex" => duplicate_exception_regex
+ )
+ end
- it { is_expected.to be(result) }
+ it { is_expected.to be(result) }
+ end
end
end
end
@@ -76,11 +78,13 @@ RSpec.describe Namespace::PackageSetting do
context 'package types without package_settings' do
[:npm_package, :conan_package, :nuget_package, :pypi_package, :composer_package, :golang_package, :debian_package].each do |format|
- let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang
- let_it_be(:package_setting) { package.project.namespace.package_settings }
+ context "with package_type:#{format}" do
+ let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang
+ let_it_be(:package_setting) { package.project.namespace.package_settings }
- it 'raises an error' do
- expect { subject }.to raise_error(Namespace::PackageSetting::PackageSettingNotImplemented)
+ it 'raises an error' do
+ expect { subject }.to raise_error(Namespace::PackageSetting::PackageSettingNotImplemented)
+ end
end
end
end
diff --git a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
index de10653d87e..a2ab59f56ab 100644
--- a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
@@ -23,8 +23,7 @@ RSpec.describe Preloaders::UserMaxAccessLevelInProjectsPreloader do
# we have an existing N+1, one for each project for which user is not a member
# in this spec, project_3, project_4, project_5
# https://gitlab.com/gitlab-org/gitlab/-/issues/362890
- ee_only_policy_check_queries = Gitlab.ee? ? 1 : 0
- expect { query }.to make_queries(projects.size + 3 + ee_only_policy_check_queries)
+ expect { query }.to make_queries(projects.size + 3)
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 72011693e20..ea237768333 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -3132,6 +3132,18 @@ RSpec.describe Repository, feature_category: :source_code_management do
2.times { repository.ancestor?(commit.id, ancestor.id) }
end
+ it 'calls out to Gitaly again after expiration' do
+ expect(repository.raw_repository).to receive(:ancestor?).once
+
+ repository.ancestor?(commit.id, ancestor.id)
+
+ repository.expire_ancestor_cache(commit.id, ancestor.id)
+
+ expect(repository.raw_repository).to receive(:ancestor?).once
+
+ 2.times { repository.ancestor?(commit.id, ancestor.id) }
+ end
+
it 'returns the value from the request store' do
repository.__send__(:request_store_cache).write(cache_key, "it's apparent")
diff --git a/spec/requests/api/integrations/slack/events_spec.rb b/spec/requests/api/integrations/slack/events_spec.rb
new file mode 100644
index 00000000000..438715db4f0
--- /dev/null
+++ b/spec/requests/api/integrations/slack/events_spec.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Integrations::Slack::Events, feature_category: :integrations do
+ describe 'POST /integrations/slack/events' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:params) { {} }
+ let(:headers) do
+ {
+ ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s,
+ ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature'
+ }
+ end
+
+ before do
+ allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature|
+ signature == 'mock_verified_signature'
+ end
+
+ stub_application_setting(slack_app_signing_secret: 'mock_key')
+ end
+
+ subject { post api('/integrations/slack/events'), params: params, headers: headers }
+
+ it_behaves_like 'Slack request verification'
+
+ context 'when type param is unknown' do
+ let(:params) do
+ { type: 'unknown_type' }
+ end
+
+ it 'generates a tracked error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).once
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(response.body).to be_empty
+ end
+ end
+
+ context 'when type param is url_verification' do
+ let(:params) do
+ {
+ type: 'url_verification',
+ challenge: '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P'
+ }
+ end
+
+ it 'responds in-request with the challenge' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({ 'challenge' => '3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P' })
+ end
+ end
+
+ context 'when event.type param is app_home_opened' do
+ let(:params) do
+ {
+ type: 'event_callback',
+ team_id: slack_installation.team_id,
+ event_id: 'Ev03SA75UJKB',
+ event: {
+ type: 'app_home_opened',
+ user: 'U0123ABCDEF'
+ }
+ }
+ end
+
+ it 'calls the Slack API (integration-style test)', :sidekiq_inline, :clean_gitlab_redis_shared_state do
+ api_url = "#{Slack::API::BASE_URL}/views.publish"
+
+ stub_request(:post, api_url)
+ .to_return(
+ status: 200,
+ body: { ok: true }.to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+
+ subject
+
+ expect(WebMock).to have_requested(:post, api_url)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response.body).to eq('{}')
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/integrations/slack/interactions_spec.rb b/spec/requests/api/integrations/slack/interactions_spec.rb
new file mode 100644
index 00000000000..35a96be75e0
--- /dev/null
+++ b/spec/requests/api/integrations/slack/interactions_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Integrations::Slack::Interactions, feature_category: :integrations do
+ describe 'POST /integrations/slack/interactions' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:payload) { {} }
+ let(:params) { { payload: Gitlab::Json.dump(payload) } }
+
+ let(:headers) do
+ {
+ ::API::Integrations::Slack::Request::VERIFICATION_TIMESTAMP_HEADER => Time.current.to_i.to_s,
+ ::API::Integrations::Slack::Request::VERIFICATION_SIGNATURE_HEADER => 'mock_verified_signature'
+ }
+ end
+
+ before do
+ allow(ActiveSupport::SecurityUtils).to receive(:secure_compare) do |signature|
+ signature == 'mock_verified_signature'
+ end
+
+ stub_application_setting(slack_app_signing_secret: 'mock_key')
+ end
+
+ subject { post api('/integrations/slack/interactions'), params: params, headers: headers }
+
+ it_behaves_like 'Slack request verification'
+
+ context 'when type param is unknown' do
+ let(:payload) do
+ { type: 'unknown_type' }
+ end
+
+ it 'generates a tracked error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).once
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ expect(response.body).to be_empty
+ end
+ end
+
+ context 'when event.type param is view_closed' do
+ let(:payload) do
+ {
+ type: 'view_closed',
+ team_id: slack_installation.team_id,
+ event: {
+ type: 'view_closed',
+ user: 'U0123ABCDEF'
+ }
+ }
+ end
+
+ it 'calls the Slack Interactivity Service' do
+ expect_next_instance_of(::Integrations::SlackInteractionService) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/admin/abuse_report_entity_spec.rb b/spec/serializers/admin/abuse_report_entity_spec.rb
index 2101fc15dd0..003d76a172f 100644
--- a/spec/serializers/admin/abuse_report_entity_spec.rb
+++ b/spec/serializers/admin/abuse_report_entity_spec.rb
@@ -11,12 +11,6 @@ RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
described_class.new(abuse_report)
end
- before do
- allow_next_instance_of(described_class) do |instance|
- allow(instance).to receive(:markdown_field).with(abuse_report, :message).and_return(abuse_report.message)
- end
- end
-
describe '#as_json' do
subject(:entity_hash) { entity.as_json }
@@ -27,71 +21,20 @@ RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
:updated_at,
:reported_user,
:reporter,
- :reported_user_path,
- :reporter_path,
- :user_blocked,
- :block_user_path,
- :remove_report_path,
- :remove_user_and_report_path,
- :message,
:report_path
)
end
it 'correctly exposes `reported user`' do
- expect(entity_hash[:reported_user].keys).to match_array([:name, :created_at])
+ expect(entity_hash[:reported_user].keys).to match_array([:name])
end
it 'correctly exposes `reporter`' do
expect(entity_hash[:reporter].keys).to match_array([:name])
end
- it 'correctly exposes :reported_user_path' do
- expect(entity_hash[:reported_user_path]).to eq user_path(abuse_report.user)
- end
-
- it 'correctly exposes :reporter_path' do
- expect(entity_hash[:reporter_path]).to eq user_path(abuse_report.reporter)
- end
-
- describe 'user_blocked' do
- subject(:user_blocked) { entity_hash[:user_blocked] }
-
- context 'when user is blocked' do
- before do
- allow(abuse_report.user).to receive(:blocked?).and_return(true)
- end
-
- it { is_expected.to be true }
- end
-
- context 'when user is not blocked' do
- before do
- allow(abuse_report.user).to receive(:blocked?).and_return(false)
- end
-
- it { is_expected.to be false }
- end
- end
-
- it 'correctly exposes :block_user_path' do
- expect(entity_hash[:block_user_path]).to eq block_admin_user_path(abuse_report.user)
- end
-
- it 'correctly exposes :remove_report_path' do
- expect(entity_hash[:remove_report_path]).to eq admin_abuse_report_path(abuse_report)
- end
-
it 'correctly exposes :report_path' do
expect(entity_hash[:report_path]).to eq admin_abuse_report_path(abuse_report)
end
-
- it 'correctly exposes :remove_user_and_report_path' do
- expect(entity_hash[:remove_user_and_report_path]).to eq admin_abuse_report_path(abuse_report, remove_user: true)
- end
-
- it 'correctly exposes :message' do
- expect(entity_hash[:message]).to eq(abuse_report.message)
- end
end
end
diff --git a/spec/services/draft_notes/publish_service_spec.rb b/spec/services/draft_notes/publish_service_spec.rb
index a4b1d8742d0..dab06637c1a 100644
--- a/spec/services/draft_notes/publish_service_spec.rb
+++ b/spec/services/draft_notes/publish_service_spec.rb
@@ -172,7 +172,12 @@ RSpec.describe DraftNotes::PublishService, feature_category: :code_review_workfl
end
end
- it 'does not requests a lot from Gitaly', :request_store do
+ it 'does not request a lot from Gitaly', :request_store, :clean_gitlab_redis_cache do
+ merge_request
+ position
+
+ Gitlab::GitalyClient.reset_counts
+
# NOTE: This should be reduced as we work on reducing Gitaly calls.
# Gitaly requests shouldn't go above this threshold as much as possible
# as it may add more to the Gitaly N+1 issue we are experiencing.
diff --git a/spec/services/integrations/slack_event_service_spec.rb b/spec/services/integrations/slack_event_service_spec.rb
new file mode 100644
index 00000000000..17433aee329
--- /dev/null
+++ b/spec/services/integrations/slack_event_service_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackEventService, feature_category: :integrations do
+ describe '#execute' do
+ subject(:execute) { described_class.new(params).execute }
+
+ let(:params) do
+ {
+ type: 'event_callback',
+ event: {
+ type: 'app_home_opened',
+ foo: 'bar'
+ }
+ }
+ end
+
+ it 'queues a worker and returns success response' do
+ expect(Integrations::SlackEventWorker).to receive(:perform_async)
+ .with(
+ {
+ slack_event: 'app_home_opened',
+ params: {
+ event: {
+ foo: 'bar'
+ }
+ }
+ }
+ )
+ expect(execute.payload).to eq({})
+ is_expected.to be_success
+ end
+
+ context 'when event a url verification request' do
+ let(:params) { { type: 'url_verification', foo: 'bar' } }
+
+ it 'executes the service instead of queueing a worker and returns success response' do
+ expect(Integrations::SlackEventWorker).not_to receive(:perform_async)
+ expect_next_instance_of(Integrations::SlackEvents::UrlVerificationService, { foo: 'bar' }) do |service|
+ expect(service).to receive(:execute).and_return({ baz: 'qux' })
+ end
+ expect(execute.payload).to eq({ baz: 'qux' })
+ is_expected.to be_success
+ end
+ end
+
+ context 'when event is unknown' do
+ let(:params) { super().merge(event: { type: 'foo' }) }
+
+ it 'raises an error' do
+ expect { execute }.to raise_error(described_class::UnknownEventError)
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_events/app_home_opened_service_spec.rb b/spec/services/integrations/slack_events/app_home_opened_service_spec.rb
new file mode 100644
index 00000000000..0eb4c019e0a
--- /dev/null
+++ b/spec/services/integrations/slack_events/app_home_opened_service_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackEvents::AppHomeOpenedService, feature_category: :integrations do
+ describe '#execute' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:slack_workspace_id) { slack_installation.team_id }
+ let(:slack_user_id) { 'U0123ABCDEF' }
+ let(:api_url) { "#{Slack::API::BASE_URL}/views.publish" }
+ let(:api_response) { { ok: true } }
+ let(:params) do
+ {
+ team_id: slack_workspace_id,
+ event: { user: slack_user_id },
+ event_id: 'Ev03SA75UJKB'
+ }
+ end
+
+ subject(:execute) { described_class.new(params).execute }
+
+ before do
+ stub_request(:post, api_url)
+ .to_return(
+ status: 200,
+ body: api_response.to_json,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ shared_examples 'there is no bot token' do
+ it 'does not call the Slack API, logs info, and returns a success response' do
+ expect(Gitlab::IntegrationsLogger).to receive(:info).with(
+ {
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id,
+ message: 'SlackInstallation record has no bot token'
+ }
+ )
+
+ is_expected.to be_success
+ end
+ end
+
+ it 'calls the Slack API correctly and returns a success response' do
+ mock_view = { type: 'home', blocks: [] }
+
+ expect_next_instance_of(Slack::BlockKit::AppHomeOpened) do |ui|
+ expect(ui).to receive(:build).and_return(mock_view)
+ end
+
+ is_expected.to be_success
+
+ expect(WebMock).to have_requested(:post, api_url).with(
+ body: {
+ user_id: slack_user_id,
+ view: mock_view
+ },
+ headers: {
+ 'Authorization' => "Bearer #{slack_installation.bot_access_token}",
+ 'Content-Type' => 'application/json; charset=utf-8'
+ })
+ end
+
+ context 'when the slack installation is a legacy record' do
+ let_it_be(:slack_installation) { create(:slack_integration, :legacy) }
+
+ it_behaves_like 'there is no bot token'
+ end
+
+ context 'when the slack installation cannot be found' do
+ let(:slack_workspace_id) { non_existing_record_id }
+
+ it_behaves_like 'there is no bot token'
+ end
+
+ context 'when the Slack API call raises an HTTP exception' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED, 'error message')
+ end
+
+ it 'tracks the exception and returns an error response' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ Errno::ECONNREFUSED.new('HTTP exception when calling Slack API'),
+ {
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id
+ }
+ )
+ is_expected.to be_error
+ end
+ end
+
+ context 'when the Slack API returns an error' do
+ let(:api_response) { { ok: false, foo: 'bar' } }
+
+ it 'tracks the exception and returns an error response' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ StandardError.new('Slack API returned an error'),
+ {
+ slack_user_id: slack_user_id,
+ slack_workspace_id: slack_workspace_id,
+ response: api_response.with_indifferent_access
+ }
+ )
+ is_expected.to be_error
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_events/url_verification_service_spec.rb b/spec/services/integrations/slack_events/url_verification_service_spec.rb
new file mode 100644
index 00000000000..0d668acafb9
--- /dev/null
+++ b/spec/services/integrations/slack_events/url_verification_service_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackEvents::UrlVerificationService, feature_category: :integrations do
+ describe '#execute' do
+ it 'returns the challenge' do
+ expect(described_class.new({ challenge: 'foo' }).execute).to eq({ challenge: 'foo' })
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_interaction_service_spec.rb b/spec/services/integrations/slack_interaction_service_spec.rb
new file mode 100644
index 00000000000..599320c7986
--- /dev/null
+++ b/spec/services/integrations/slack_interaction_service_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractionService, feature_category: :integrations do
+ describe '#execute' do
+ subject(:execute) { described_class.new(params).execute }
+
+ let(:params) do
+ {
+ type: slack_interaction,
+ foo: 'bar'
+ }
+ end
+
+ context 'when view is closed' do
+ let(:slack_interaction) { 'view_closed' }
+
+ it 'executes the correct service' do
+ view_closed_service = described_class::INTERACTIONS['view_closed']
+
+ expect_next_instance_of(view_closed_service, { foo: 'bar' }) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ context 'when view is submitted' do
+ let(:slack_interaction) { 'view_submission' }
+
+ it 'executes the submission service' do
+ view_submission_service = described_class::INTERACTIONS['view_submission']
+
+ expect_next_instance_of(view_submission_service, { foo: 'bar' }) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ context 'when block action service is submitted' do
+ let(:slack_interaction) { 'block_actions' }
+
+ it 'executes the block actions service' do
+ block_action_service = described_class::INTERACTIONS['block_actions']
+
+ expect_next_instance_of(block_action_service, { foo: 'bar' }) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ context 'when slack_interaction is not known' do
+ let(:slack_interaction) { 'foo' }
+
+ it 'raises an error and does not execute a service class' do
+ described_class::INTERACTIONS.each_value do |service_class|
+ expect(service_class).not_to receive(:new)
+ end
+
+ expect { execute }.to raise_error(described_class::UnknownInteractionError)
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_interactions/block_action_service_spec.rb b/spec/services/integrations/slack_interactions/block_action_service_spec.rb
new file mode 100644
index 00000000000..9a188ddcfe4
--- /dev/null
+++ b/spec/services/integrations/slack_interactions/block_action_service_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractions::BlockActionService, feature_category: :integrations do
+ describe '#execute' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+
+ let(:params) do
+ {
+ view: {
+ team_id: slack_installation.team_id
+ },
+ actions: [{
+ action_id: action_id
+ }]
+ }
+ end
+
+ subject(:execute) { described_class.new(params).execute }
+
+ context 'when action_id is incident_management_project' do
+ let(:action_id) { 'incident_management_project' }
+
+ it 'executes the correct handler' do
+ project_handler = described_class::ALLOWED_UPDATES_HANDLERS['incident_management_project']
+
+ expect_next_instance_of(project_handler, params, params[:actions].first) do |handler|
+ expect(handler).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ execute
+ end
+ end
+
+ context 'when action_id is not known' do
+ let(:action_id) { 'random' }
+
+ it 'does not execute the handlers' do
+ described_class::ALLOWED_UPDATES_HANDLERS.each_value do |handler_class|
+ expect(handler_class).not_to receive(:new)
+ end
+
+ execute
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_interactions/incident_management/incident_modal_closed_service_spec.rb b/spec/services/integrations/slack_interactions/incident_management/incident_modal_closed_service_spec.rb
new file mode 100644
index 00000000000..64cddf9a66b
--- /dev/null
+++ b/spec/services/integrations/slack_interactions/incident_management/incident_modal_closed_service_spec.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractions::IncidentManagement::IncidentModalClosedService,
+ feature_category: :integrations do
+ describe '#execute' do
+ let_it_be(:request_body) do
+ {
+ replace_original: 'true',
+ text: 'Incident creation cancelled.'
+ }
+ end
+
+ let(:params) do
+ {
+ view: {
+ private_metadata: 'https://api.slack.com/id/1234'
+ }
+ }
+ end
+
+ let(:service) { described_class.new(params) }
+
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return({ ok: true })
+ end
+
+ context 'when executed' do
+ it 'makes the POST call and closes the modal' do
+ expect(Gitlab::HTTP).to receive(:post).with(
+ 'https://api.slack.com/id/1234',
+ body: Gitlab::Json.dump(request_body),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+
+ service.execute
+ end
+ end
+
+ context 'when the POST call raises an HTTP exception' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED, 'error message')
+ end
+
+ it 'tracks the exception and returns an error response' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ Errno::ECONNREFUSED.new('HTTP exception when calling Slack API'),
+ {
+ params: params
+ }
+ )
+
+ service.execute
+ end
+ end
+
+ context 'when response is not ok' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_return({ ok: false })
+ end
+
+ it 'returns error response and tracks the exception' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ StandardError.new('Something went wrong while closing the incident form.'),
+ {
+ response: { ok: false },
+ params: params
+ }
+ )
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb b/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb
new file mode 100644
index 00000000000..adaeadaa997
--- /dev/null
+++ b/spec/services/integrations/slack_interactions/incident_management/incident_modal_submit_service_spec.rb
@@ -0,0 +1,296 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractions::IncidentManagement::IncidentModalSubmitService,
+ feature_category: :incident_management do
+ include Gitlab::Routing
+
+ describe '#execute' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:api_url) { 'https://api.slack.com/id/1234' }
+
+ let_it_be(:chat_name) do
+ create(:chat_name,
+ user: user,
+ team_id: slack_installation.team_id,
+ chat_id: slack_installation.user_id
+ )
+ end
+
+ # Setting below params as they are optional, have added values wherever required in specs
+ let(:zoom_link) { '' }
+ let(:severity) { {} }
+ let(:status) { '' }
+ let(:assignee_id) { nil }
+ let(:selected_label_ids) { [] }
+ let(:label_ids) { { selected_options: selected_label_ids } }
+ let(:confidential_selected_options) { [] }
+ let(:confidential) { { selected_options: confidential_selected_options } }
+ let(:title) { 'Incident title' }
+
+ let(:zoom) do
+ {
+ link: {
+ value: zoom_link
+ }
+ }
+ end
+
+ let(:params) do
+ {
+ team: {
+ id: slack_installation.team_id
+ },
+ user: {
+ id: slack_installation.user_id
+ },
+ view: {
+ private_metadata: api_url,
+ state: {
+ values: {
+ title_input: {
+ title: {
+ value: title
+ }
+ },
+ incident_description: {
+ description: {
+ value: 'Incident description'
+ }
+ },
+ project_and_severity_selector: {
+ incident_management_project: {
+ selected_option: {
+ value: project.id.to_s
+ }
+ },
+ severity: severity
+ },
+ confidentiality: {
+ confidential: confidential
+ },
+ zoom: zoom,
+ status_and_assignee_selector: {
+ status: {
+ selected_option: {
+ value: status
+ }
+ },
+ assignee: {
+ selected_option: {
+ value: assignee_id
+ }
+ }
+ },
+ label_selector: {
+ labels: label_ids
+ }
+ }
+ }
+ }
+ }
+ end
+
+ subject(:execute_service) { described_class.new(params).execute }
+
+ shared_examples 'error in creation' do |error_message|
+ it 'returns error and raises exception' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ described_class::IssueCreateError.new(error_message),
+ {
+ slack_workspace_id: slack_installation.team_id,
+ slack_user_id: slack_installation.user_id
+ }
+ )
+
+ expect(Gitlab::HTTP).to receive(:post)
+ .with(
+ api_url,
+ body: Gitlab::Json.dump(
+ {
+ replace_original: 'true',
+ text: 'There was a problem creating the incident. Please try again.'
+ }
+ ),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+
+ response = execute_service
+
+ expect(response).to be_error
+ expect(response.message).to eq(error_message)
+ end
+ end
+
+ context 'when user has permissions to create incidents' do
+ let(:api_response) { '{"ok":true}' }
+
+ before do
+ project.add_developer(user)
+ stub_request(:post, api_url)
+ .to_return(body: api_response, headers: { 'Content-Type' => 'application/json' })
+ end
+
+ context 'with markup string in title' do
+ let(:title) { '<a href="url">incident title</a>' }
+ let(:incident) { create(:incident, title: title, project: project) }
+
+ before do
+ allow_next_instance_of(Issues::CreateService) do |service|
+ allow(service).to receive(:execute).and_return(
+ ServiceResponse.success(payload: { issue: incident, error: [] })
+ )
+ end
+ end
+
+ it 'strips the markup and saves sends the title' do
+ expect(Gitlab::HTTP).to receive(:post)
+ .with(
+ api_url,
+ body: Gitlab::Json.dump(
+ {
+ replace_original: 'true',
+ text: "New incident has been created: " \
+ "<#{issue_url(incident)}|#{incident.to_reference} - a href=\"url\"incident title/a>. "
+ }
+ ),
+ headers: { 'Content-Type' => 'application/json' }
+ ).and_return(api_response)
+
+ execute_service
+ end
+ end
+
+ context 'with non-optional params' do
+ it 'creates incident' do
+ response = execute_service
+ incident = response[:incident]
+
+ expect(response).to be_success
+ expect(incident).not_to be_nil
+ expect(incident.description).to eq('Incident description')
+ expect(incident.author).to eq(user)
+ expect(incident.severity).to eq('unknown')
+ expect(incident.confidential).to be_falsey
+ expect(incident.escalation_status).to be_triggered
+ end
+
+ it 'sends incident link to slack' do
+ execute_service
+
+ expect(WebMock).to have_requested(:post, api_url)
+ end
+ end
+
+ context 'with zoom_link' do
+ let(:zoom_link) { 'https://gitlab.zoom.us/j/1234' }
+
+ it 'sets zoom link as quick action' do
+ incident = execute_service[:incident]
+ zoom_meeting = ZoomMeeting.find_by_issue_id(incident.id)
+
+ expect(incident.description).to eq("Incident description")
+ expect(zoom_meeting.url).to eq(zoom_link)
+ end
+ end
+
+ context 'with confidential and severity' do
+ let(:confidential_selected_options) { ['confidential'] }
+ let(:severity) do
+ {
+ selected_option: {
+ value: 'high'
+ }
+ }
+ end
+
+ it 'sets confidential and severity' do
+ incident = execute_service[:incident]
+
+ expect(incident.confidential).to be_truthy
+ expect(incident.severity).to eq('high')
+ end
+ end
+
+ context 'with incident status' do
+ let(:status) { 'resolved' }
+
+ it 'sets the incident status' do
+ incident = execute_service[:incident]
+
+ expect(incident.escalation_status).to be_resolved
+ end
+ end
+
+ context 'with assignee id' do
+ let(:assignee_id) { user.id.to_s }
+
+ it 'assigns the incident to user' do
+ incident = execute_service[:incident]
+
+ expect(incident.assignees).to contain_exactly(user)
+ end
+
+ context 'when user is not a member of the project' do
+ let(:assignee_id) { create(:user).id.to_s }
+
+ it 'does not assign the user' do
+ incident = execute_service[:incident]
+
+ expect(incident.assignees).to be_empty
+ end
+ end
+ end
+
+ context 'with label ids' do
+ let_it_be(:project_label1) { create(:label, project: project, title: 'Label 1') }
+ let_it_be(:project_label2) { create(:label, project: project, title: 'Label 2') }
+
+ let(:selected_label_ids) do
+ [
+ { value: project_label1.id.to_s },
+ { value: project_label2.id.to_s }
+ ]
+ end
+
+ it 'assigns the label to the incident' do
+ incident = execute_service[:incident]
+
+ expect(incident.labels).to contain_exactly(project_label1, project_label2)
+ end
+ end
+
+ context 'when response is not ok' do
+ let(:api_response) { '{"ok":false}' }
+
+ it 'returns error response and tracks the exception' do
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ StandardError.new('Something went wrong when sending the incident link to Slack.'),
+ {
+ response: { 'ok' => false },
+ slack_workspace_id: slack_installation.team_id,
+ slack_user_id: slack_installation.user_id
+ }
+ )
+
+ execute_service
+ end
+ end
+
+ context 'when incident creation fails' do
+ let(:title) { '' }
+
+ it_behaves_like 'error in creation', "Title can't be blank"
+ end
+ end
+
+ context 'when user does not have permission to create incidents' do
+ it_behaves_like 'error in creation', 'Operation not allowed'
+ end
+ end
+end
diff --git a/spec/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler_spec.rb b/spec/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler_spec.rb
new file mode 100644
index 00000000000..5edffc99977
--- /dev/null
+++ b/spec/services/integrations/slack_interactions/slack_block_actions/incident_management/project_update_handler_spec.rb
@@ -0,0 +1,158 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackInteractions::SlackBlockActions::IncidentManagement::ProjectUpdateHandler,
+ feature_category: :incident_management do
+ describe '#execute' do
+ let_it_be(:slack_installation) { create(:slack_integration) }
+ let_it_be(:old_project) { create(:project) }
+ let_it_be(:new_project) { create(:project) }
+ let_it_be(:user) { create(:user, developer_projects: [old_project, new_project]) }
+ let_it_be(:chat_name) { create(:chat_name, user: user) }
+ let_it_be(:api_url) { "#{Slack::API::BASE_URL}/views.update" }
+
+ let(:block) do
+ {
+ block_id: 'incident_description',
+ element: {
+ initial_value: ''
+ }
+ }
+ end
+
+ let(:view) do
+ {
+ id: 'V04EQH1SP27',
+ team_id: slack_installation.team_id,
+ blocks: [block]
+ }
+ end
+
+ let(:action) do
+ {
+ selected_option: {
+ value: new_project.id.to_s
+ }
+ }
+ end
+
+ let(:params) do
+ {
+ view: view,
+ user: {
+ id: slack_installation.user_id
+ }
+ }
+ end
+
+ before do
+ allow_next_instance_of(ChatNames::FindUserService) do |user_service|
+ allow(user_service).to receive(:execute).and_return(chat_name)
+ end
+
+ stub_request(:post, api_url)
+ .to_return(
+ status: 200,
+ body: Gitlab::Json.dump({ ok: true }),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ shared_examples 'does not make api call' do
+ it 'does not make the api call and returns nil' do
+ expect(Rails.cache).to receive(:read).and_return(project.id.to_s)
+ expect(Rails.cache).not_to receive(:write)
+
+ expect(execute).to be_nil
+ expect(WebMock).not_to have_requested(:post, api_url)
+ end
+ end
+
+ subject(:execute) { described_class.new(params, action).execute }
+
+ context 'when project is updated' do
+ it 'returns success response and updates cache' do
+ expect(Rails.cache).to receive(:read).and_return(old_project.id.to_s)
+ expect(Rails.cache).to receive(:write).with(
+ "slack:incident_modal_opened:#{view[:id]}",
+ new_project.id.to_s,
+ expires_in: 5.minutes
+ )
+
+ expect(execute.message).to eq('Modal updated')
+
+ updated_block = block.dup
+ updated_block[:block_id] = new_project.id.to_s
+ view[:blocks] = [updated_block]
+
+ expect(WebMock).to have_requested(:post, api_url).with(
+ body: {
+ view_id: view[:id],
+ view: view.except!(:team_id, :id)
+ },
+ headers: {
+ 'Authorization' => "Bearer #{slack_installation.bot_access_token}",
+ 'Content-Type' => 'application/json; charset=utf-8'
+ })
+ end
+ end
+
+ context 'when project is unchanged' do
+ it_behaves_like 'does not make api call' do
+ let(:project) { new_project }
+ end
+ end
+
+ context 'when user does not have permission to read a project' do
+ it_behaves_like 'does not make api call' do
+ let(:project) { create(:project) }
+ end
+ end
+
+ context 'when api response is not ok' do
+ before do
+ stub_request(:post, api_url)
+ .to_return(
+ status: 404,
+ body: Gitlab::Json.dump({ ok: false }),
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ it 'returns error response' do
+ expect(Rails.cache).to receive(:read).and_return(old_project.id.to_s)
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ StandardError.new('Something went wrong while updating the modal.'),
+ {
+ response: { "ok" => false },
+ slack_workspace_id: slack_installation.team_id,
+ slack_user_id: slack_installation.user_id
+ }
+ )
+
+ expect(execute.message).to eq('Something went wrong while updating the modal.')
+ end
+ end
+
+ context 'when Slack API call raises an HTTP exception' do
+ before do
+ allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED, 'error message')
+ end
+
+ it 'tracks the exception and returns an error message' do
+ expect(Rails.cache).to receive(:read).and_return(old_project.id.to_s)
+ expect(::Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(
+ Errno::ECONNREFUSED.new('HTTP exception when calling Slack API'),
+ {
+ slack_workspace_id: slack_installation.team_id
+ }
+ )
+
+ expect(execute).to be_error
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
index 675b458c435..77056cbe541 100644
--- a/spec/services/merge_requests/reload_diffs_service_spec.rb
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -34,8 +34,9 @@ RSpec.describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_
context 'cache clearing' do
it 'clears the cache for older diffs on the merge request' do
- expect_any_instance_of(Redis).to receive(:del).once.and_call_original
- expect(Rails.cache).to receive(:delete).once.and_call_original
+ expect_next_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiff) do |instance|
+ expect(instance).to receive(:clear_cache).and_call_original
+ end
subject.execute
end
diff --git a/spec/views/shared/_label_row.html.haml_spec.rb b/spec/views/shared/_label_row.html.haml_spec.rb
index 6fe74b6633b..eb277930c1d 100644
--- a/spec/views/shared/_label_row.html.haml_spec.rb
+++ b/spec/views/shared/_label_row.html.haml_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'shared/_label_row.html.haml' do
end
it 'shows the path from where the label was created' do
- expect(rendered).to have_css('.label-badge', text: project.full_name)
+ expect(rendered).to have_text(project.full_name)
end
end
@@ -70,7 +70,7 @@ RSpec.describe 'shared/_label_row.html.haml' do
end
it 'shows the path from where the label was created' do
- expect(rendered).to have_css('.label-badge', text: subgroup.full_name)
+ expect(rendered).to have_text(subgroup.full_name)
end
end
diff --git a/spec/workers/integrations/slack_event_worker_spec.rb b/spec/workers/integrations/slack_event_worker_spec.rb
new file mode 100644
index 00000000000..6754801a2bd
--- /dev/null
+++ b/spec/workers/integrations/slack_event_worker_spec.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Integrations::SlackEventWorker, :clean_gitlab_redis_shared_state, feature_category: :integrations do
+ describe '.event?' do
+ subject { described_class.event?(event) }
+
+ context 'when event is known' do
+ let(:event) { 'app_home_opened' }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when event is not known' do
+ let(:event) { 'foo' }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#perform' do
+ let(:worker) { described_class.new }
+ let(:event) { 'app_home_opened' }
+ let(:service_class) { ::Integrations::SlackEvents::AppHomeOpenedService }
+
+ let(:args) do
+ {
+ slack_event: event,
+ params: params
+ }
+ end
+
+ let(:params) do
+ {
+ team_id: "T0123A456BC",
+ event: { user: "U0123ABCDEF" },
+ event_id: "Ev03SA75UJKB"
+ }
+ end
+
+ shared_examples 'logs extra metadata on done' do
+ specify do
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:slack_event, event)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:slack_user_id, 'U0123ABCDEF')
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:slack_workspace_id, 'T0123A456BC')
+
+ worker.perform(args)
+ end
+ end
+
+ it 'executes the correct service' do
+ expect_next_instance_of(service_class, params) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ worker.perform(args)
+ end
+
+ it_behaves_like 'logs extra metadata on done'
+
+ it_behaves_like 'an idempotent worker' do
+ let(:job_args) { [args] }
+ end
+
+ it 'ensures idempotency when called twice by only executing service once' do
+ expect_next_instances_of(service_class, 1, params) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ worker.perform(args)
+ worker.perform(args)
+ end
+
+ it 'executes service twice if service returned an error' do
+ expect_next_instances_of(service_class, 2, params) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
+ end
+
+ worker.perform(args)
+ worker.perform(args)
+ end
+
+ it 'executes service twice if service raised an error' do
+ expect_next_instances_of(service_class, 2, params) do |service|
+ expect(service).to receive(:execute).and_raise(ArgumentError)
+ end
+
+ expect { worker.perform(args) }.to raise_error(ArgumentError)
+ expect { worker.perform(args) }.to raise_error(ArgumentError)
+ end
+
+ it 'executes service twice when event_id is different' do
+ second_params = params.dup
+ second_args = args.dup
+ second_params[:event_id] = 'foo'
+ second_args[:params] = second_params
+
+ expect_next_instances_of(service_class, 1, params) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ expect_next_instances_of(service_class, 1, second_params) do |service|
+ expect(service).to receive(:execute).and_return(ServiceResponse.success)
+ end
+
+ worker.perform(args)
+ worker.perform(second_args)
+ end
+
+ context 'when event is not known' do
+ let(:event) { 'foo' }
+
+ it 'does not execute the service class' do
+ expect(service_class).not_to receive(:new)
+
+ worker.perform(args)
+ end
+
+ it 'logs an error' do
+ expect(Sidekiq.logger).to receive(:error).with({ message: 'Unknown slack_event', slack_event: event })
+
+ worker.perform(args)
+ end
+
+ it_behaves_like 'logs extra metadata on done'
+ end
+ end
+end
diff --git a/vendor/gems/bundler-checksum/README.md b/vendor/gems/bundler-checksum/README.md
index 675c3ad2ee8..ba9b7c5a25f 100644
--- a/vendor/gems/bundler-checksum/README.md
+++ b/vendor/gems/bundler-checksum/README.md
@@ -7,7 +7,7 @@ Bundler patch for verifying local gem checksums
Add the following to your Gemfile:
```
-if ENV['BUNDLER_CHECKSUM_VERIFICATION_OPT_IN'] # this verification is still experimental
+if ENV.fetch('BUNDLER_CHECKSUM_VERIFICATION_OPT_IN', 'false') != 'false' # this verification is still experimental
require 'bundler-checksum'
BundlerChecksum.patch!
end
diff --git a/vendor/gems/bundler-checksum/test/project_with_checksum_lock/Gemfile b/vendor/gems/bundler-checksum/test/project_with_checksum_lock/Gemfile
index 503cf4587fa..6787ee0521a 100644
--- a/vendor/gems/bundler-checksum/test/project_with_checksum_lock/Gemfile
+++ b/vendor/gems/bundler-checksum/test/project_with_checksum_lock/Gemfile
@@ -2,7 +2,7 @@
source 'https://rubygems.org'
-if ENV['BUNDLER_CHECKSUM_VERIFICATION_OPT_IN'] # this verification is still experimental
+if ENV.fetch('BUNDLER_CHECKSUM_VERIFICATION_OPT_IN', 'false') != 'false' # this verification is still experimental
$:.unshift(File.expand_path('../../lib', __dir__))
require 'bundler-checksum'
BundlerChecksum.patch!
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index bd746dbfb5b..3babc60a5bc 100644
--- a/vendor/project_templates/express.tar.gz
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/workhorse/internal/headers/content_headers.go b/workhorse/internal/headers/content_headers.go
index 854cc8abddd..54c7c1bdd95 100644
--- a/workhorse/internal/headers/content_headers.go
+++ b/workhorse/internal/headers/content_headers.go
@@ -1,6 +1,7 @@
package headers
import (
+ "mime"
"net/http"
"regexp"
@@ -13,8 +14,9 @@ var (
imageTypeRegex = regexp.MustCompile(`^image/*`)
svgMimeTypeRegex = regexp.MustCompile(`^image/svg\+xml$`)
- textTypeRegex = regexp.MustCompile(`^text/*`)
-
+ textTypeRegex = regexp.MustCompile(`^text/*`)
+ xmlTypeRegex = regexp.MustCompile(`^text/xml`)
+ xhtmlTypeRegex = regexp.MustCompile(`^text/html`)
videoTypeRegex = regexp.MustCompile(`^video/*`)
pdfTypeRegex = regexp.MustCompile(`application\/pdf`)
@@ -26,6 +28,8 @@ var (
// Mime types that can't be inlined. Usually subtypes of main types
var forbiddenInlineTypes = []*regexp.Regexp{svgMimeTypeRegex}
+var htmlRenderingTypes = []*regexp.Regexp{xmlTypeRegex, xhtmlTypeRegex}
+
// Mime types that can be inlined. We can add global types like "image/" or
// specific types like "text/plain". If there is a specific type inside a global
// allowed type that can't be inlined we must add it to the forbiddenInlineTypes var.
@@ -38,12 +42,28 @@ const (
textPlainContentType = "text/plain; charset=utf-8"
attachmentDispositionText = "attachment"
inlineDispositionText = "inline"
+ dummyFilename = "blob"
)
func SafeContentHeaders(data []byte, contentDisposition string) (string, string) {
- contentType := safeContentType(data)
+ detectedContentType := detectContentType(data)
+
+ contentType := safeContentType(detectedContentType)
contentDisposition = safeContentDisposition(contentType, contentDisposition)
+ // Some browsers will render XML inline unless a filename directive is provided with a non-xml file extension
+ // This overrides the filename directive in the case of XML data
+ for _, element := range htmlRenderingTypes {
+ if isType(detectedContentType, element) {
+ disposition, directives, err := mime.ParseMediaType(contentDisposition)
+ if err == nil {
+ directives["filename"] = dummyFilename
+ contentDisposition = mime.FormatMediaType(disposition, directives)
+ break
+ }
+ }
+ }
+
// Set attachments to application/octet-stream since browsers can do
// a better job distinguishing certain types (for example: ZIP files
// vs. Microsoft .docx files). However, browsers may safely render SVGs even
@@ -56,15 +76,17 @@ func SafeContentHeaders(data []byte, contentDisposition string) (string, string)
return contentType, contentDisposition
}
-func safeContentType(data []byte) string {
+func detectContentType(data []byte) string {
// Special case for svg because DetectContentType detects it as text
if svg.Is(data) {
return svgContentType
}
// Override any existing Content-Type header from other ResponseWriters
- contentType := http.DetectContentType(data)
+ return http.DetectContentType(data)
+}
+func safeContentType(contentType string) string {
// http.DetectContentType does not support JavaScript and would only
// return text/plain. But for cautionary measures, just in case they start supporting
// it down the road and start returning application/javascript, we want to handle it now
diff --git a/workhorse/internal/headers/content_headers_test.go b/workhorse/internal/headers/content_headers_test.go
new file mode 100644
index 00000000000..7cfce335d88
--- /dev/null
+++ b/workhorse/internal/headers/content_headers_test.go
@@ -0,0 +1,56 @@
+package headers
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func fileContents(fileName string) []byte {
+ fileContents, _ := os.ReadFile(fileName)
+ return fileContents
+}
+
+func TestHeaders(t *testing.T) {
+ tests := []struct {
+ desc string
+ fileContents []byte
+ expectedContentType string
+ expectedContentDisposition string
+ }{
+ {
+ desc: "XML file",
+ fileContents: fileContents("../../testdata/test.xml"),
+ expectedContentType: "text/plain; charset=utf-8",
+ expectedContentDisposition: "inline; filename=blob",
+ },
+ {
+ desc: "XHTML file",
+ fileContents: fileContents("../../testdata/index.xhtml"),
+ expectedContentType: "text/plain; charset=utf-8",
+ expectedContentDisposition: "inline; filename=blob",
+ },
+ {
+ desc: "svg+xml file",
+ fileContents: fileContents("../../testdata/xml.svg"),
+ expectedContentType: "image/svg+xml",
+ expectedContentDisposition: "attachment",
+ },
+ {
+ desc: "text file",
+ fileContents: []byte(`a text file`),
+ expectedContentType: "text/plain; charset=utf-8",
+ expectedContentDisposition: "inline",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.desc, func(t *testing.T) {
+ contentType, newContentDisposition := SafeContentHeaders(test.fileContents, "")
+
+ require.Equal(t, test.expectedContentType, contentType)
+ require.Equal(t, test.expectedContentDisposition, newContentDisposition)
+ })
+ }
+}
diff --git a/workhorse/internal/senddata/contentprocessor/contentprocessor_test.go b/workhorse/internal/senddata/contentprocessor/contentprocessor_test.go
index b04263de6b9..e863935be6f 100644
--- a/workhorse/internal/senddata/contentprocessor/contentprocessor_test.go
+++ b/workhorse/internal/senddata/contentprocessor/contentprocessor_test.go
@@ -51,13 +51,13 @@ func TestSetProperContentTypeAndDisposition(t *testing.T) {
{
desc: "HTML type",
contentType: "text/plain; charset=utf-8",
- contentDisposition: "inline",
+ contentDisposition: "inline; filename=blob",
body: "<html><body>Hello world!</body></html>",
},
{
desc: "Javascript within HTML type",
contentType: "text/plain; charset=utf-8",
- contentDisposition: "inline",
+ contentDisposition: "inline; filename=blob",
body: "<script>alert(\"foo\")</script>",
},
{
diff --git a/workhorse/testdata/index.xhtml b/workhorse/testdata/index.xhtml
new file mode 100644
index 00000000000..1dd50a70e69
--- /dev/null
+++ b/workhorse/testdata/index.xhtml
@@ -0,0 +1,9 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Title of document</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/workhorse/testdata/test.xml b/workhorse/testdata/test.xml
new file mode 100644
index 00000000000..54b94e62355
--- /dev/null
+++ b/workhorse/testdata/test.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<html>
+ <head></head>
+ <body>
+ </body>
+</html> \ No newline at end of file
diff --git a/workhorse/testdata/xml.svg b/workhorse/testdata/xml.svg
new file mode 100644
index 00000000000..c41c4c44b49
--- /dev/null
+++ b/workhorse/testdata/xml.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+
+<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
+ <polygon id="triangle" points="0,0 0,50 50,0" fill="#009900" stroke="#004400"/>
+ <div>hello this is html</div>
+</svg>
diff --git a/yarn.lock b/yarn.lock
index ec89202c67e..c46ffb26473 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1115,10 +1115,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.43.0.tgz#7789e6e5e8cd7d97489d9cfb021e0f25ddcfa829"
integrity sha512-o5P8T42qXh38DU0Px7rnVCV86cDfrsKHNczdNQAIGeyw5Ci7orsL/0f1M4BVtOSgU0VOoHuB0Yb/HyQjjmwt6A==
-"@gitlab/ui@62.5.1":
- version "62.5.1"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-62.5.1.tgz#81cb569fb4a93cd2fb8eec7f2773ed13faf31638"
- integrity sha512-FjgoueUnHRI+cwsHmJO6BN50cZ7GpNxpeTQtC/tpHak1hdUty6W7t3dqZAD0pwIYutRLm8Wyxyt88k6Ki9A78A==
+"@gitlab/ui@62.5.2":
+ version "62.5.2"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-62.5.2.tgz#2a476e04d86ba4c964779cadeb7f3c4acb85ab9d"
+ integrity sha512-pTOjFRuy9KV2U1sdTC8gY3Ue4stfzuhxK9XUgs2ZieOb56XwDdDK3aP6l5JYEXsYHLpgyx7OyJuS3qJbMKvXvA==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.23.1"