From 80b22a4413679216b470c7a4e9fefd0eb928add5 Mon Sep 17 00:00:00 2001
From: GitLab Bot
Date: Fri, 24 Nov 2023 18:09:57 +0000
Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master
---
.rubocop_todo/capybara/testid_finders.yml | 22 --
.rubocop_todo/gitlab/namespaced_class.yml | 1 -
.../layout/empty_line_after_magic_comment.yml | 1 -
.rubocop_todo/rspec/context_wording.yml | 1 -
.rubocop_todo/rspec/feature_category.yml | 2 -
.rubocop_todo/rspec/named_subject.yml | 4 -
.../style/inline_disable_annotation.yml | 1 -
.../components/list/catalog_header.vue | 7 +-
app/assets/javascripts/clone_panel.js | 2 +-
.../folder/environments_folder_app.vue | 21 ++
.../folder/environments_folder_bundle.js | 27 ++-
.../graphql/resolvers/kubernetes.js | 7 +-
app/assets/javascripts/gfm_auto_complete.js | 35 +++
.../graphql/helpers/resolver_helpers.js | 6 +-
.../components/notes_activity_header.vue | 4 +-
.../search/sidebar/components/app.vue | 5 +-
.../stylesheets/page_bundles/project.scss | 2 +-
.../projects/environments_controller.rb | 4 +
app/models/product_analytics_event.rb | 37 ----
app/models/project.rb | 4 -
app/policies/user_policy.rb | 1 +
.../build_activity_graph_service.rb | 13 --
.../product_analytics/build_graph_service.rb | 33 ---
app/views/projects/_activity.html.haml | 4 +-
.../{_clone.html.haml => _code.html.haml} | 16 +-
.../projects/buttons/_download.html.haml | 20 +-
.../buttons/_download_links.html.haml | 9 +-
.../buttons/_download_menu_items.html.haml | 21 ++
app/views/projects/empty.html.haml | 6 +-
.../projects/tree/_tree_header.html.haml | 10 +-
app/views/shared/_clone_panel.html.haml | 4 +-
.../shared/_mobile_clone_panel.html.haml | 4 +-
.../development/ci_retry_on_exit_codes.yml | 8 +
.../environments_folder_new_look.yml | 8 +
config/initializers/circuitbox.rb | 8 +-
.../27_product_analytics_events.rb | 56 -----
...ake_profile_private_application_setting.rb | 9 +
...smtp_columns_from_service_desk_settings.rb | 32 +++
db/schema_migrations/20231029142649 | 1 +
db/schema_migrations/20231122100006 | 1 +
db/structure.sql | 10 +-
doc/administration/logs/index.md | 2 +-
doc/administration/monitoring/health_check.md | 2 +-
doc/administration/monitoring/index.md | 2 +-
.../performance/gitlab_configuration.md | 2 +-
.../performance/grafana_configuration.md | 2 +-
doc/administration/reply_by_email.md | 2 +-
.../incident_management_rate_limits.md | 2 +-
doc/api/alert_management_alerts.md | 2 +-
doc/api/group_ssh_certificates.md | 3 +-
doc/api/metrics_dashboard_annotations.md | 2 +-
doc/api/metrics_user_starred_dashboards.md | 2 +-
doc/development/distributed_tracing.md | 2 +-
doc/development/logging.md | 2 +-
doc/development/migration_style_guide.md | 7 +-
doc/development/prometheus_metrics.md | 2 +-
doc/development/runner_fleet_dashboard.md | 1 +
doc/operations/incident_management/alerts.md | 2 +-
.../escalation_policies.md | 2 +-
.../incident_timeline_events.md | 2 +-
.../incident_management/incidents.md | 2 +-
doc/operations/incident_management/index.md | 2 +-
.../incident_management/integrations.md | 2 +-
.../incident_management/linked_resources.md | 2 +-
.../incident_management/manage_incidents.md | 2 +-
.../incident_management/oncall_schedules.md | 2 +-
doc/operations/incident_management/paging.md | 2 +-
doc/operations/incident_management/slack.md | 2 +-
.../incident_management/status_page.md | 2 +-
doc/operations/index.md | 2 +-
doc/user/crm/index.md | 2 +-
doc/user/group/ssh_certificates.md | 83 ++++++++
doc/user/project/service_desk/configure.md | 2 +-
doc/user/project/service_desk/index.md | 2 +-
.../service_desk/using_service_desk.md | 2 +-
lib/gitlab/ci/config/entry/retry.rb | 14 +-
lib/gitlab/circuit_breaker.rb | 36 ++++
lib/gitlab/circuit_breaker/notifier.rb | 28 +++
lib/gitlab/circuit_breaker/store.rb | 51 +++++
.../quick_actions/merge_request_actions.rb | 17 +-
lib/product_analytics/event_params.rb | 56 -----
qa/qa/page/component/clone_panel.rb | 2 +-
rubocop/rubocop-code_reuse.yml | 1 +
spec/factories/environments.rb | 8 +
spec/factories/product_analytics_event.rb | 24 ---
spec/features/dashboard/projects_spec.rb | 4 +-
spec/features/dashboard/todos/todos_spec.rb | 10 +-
.../environments/environments_folder_spec.rb | 46 ++++
spec/features/groups/board_sidebar_spec.rb | 2 +-
spec/features/groups/board_spec.rb | 2 +-
spec/features/groups/clusters/user_spec.rb | 4 +-
spec/features/groups/dependency_proxy_spec.rb | 2 +-
spec/features/groups/group_settings_spec.rb | 8 +-
.../groups/members/leave_group_spec.rb | 2 +-
.../groups/members/manage_groups_spec.rb | 2 +-
...r_adds_member_with_expiration_date_spec.rb | 2 +-
.../groups/members/search_members_spec.rb | 2 +-
.../groups/members/sort_members_spec.rb | 2 +-
spec/features/groups/members/tabs_spec.rb | 4 +-
spec/features/groups/merge_requests_spec.rb | 2 +-
.../environments/environments_spec.rb | 16 --
.../projects/user_sees_sidebar_spec.rb | 2 +-
.../components/list/catalog_header_spec.js | 14 ++
.../folder/environments_folder_app_spec.js | 23 ++
spec/frontend/gfm_auto_complete_spec.js | 72 +++++--
.../search/sidebar/components/app_spec.js | 5 -
spec/initializers/circuitbox_spec.rb | 7 +-
spec/lib/gitlab/ci/config/entry/job_spec.rb | 89 +++++++-
spec/lib/gitlab/ci/config/entry/retry_spec.rb | 86 +++++++-
.../gitlab/circuit_breaker/notifier_spec.rb | 37 ++++
spec/lib/gitlab/circuit_breaker/store_spec.rb | 201 ++++++++++++++++++
spec/lib/gitlab/circuit_breaker_spec.rb | 120 +++++++++++
.../product_analytics/event_params_spec.rb | 59 -----
spec/models/product_analytics_event_spec.rb | 52 -----
.../build_activity_graph_service_spec.rb | 33 ---
.../build_graph_service_spec.rb | 27 ---
.../quick_actions/interpret_service_spec.rb | 30 +++
spec/support/rspec_order_todo.yml | 4 -
.../projects/tags/index.html.haml_spec.rb | 5 +-
119 files changed, 1215 insertions(+), 619 deletions(-)
create mode 100644 app/assets/javascripts/environments/folder/environments_folder_app.vue
delete mode 100644 app/models/product_analytics_event.rb
delete mode 100644 app/services/product_analytics/build_activity_graph_service.rb
delete mode 100644 app/services/product_analytics/build_graph_service.rb
rename app/views/projects/buttons/{_clone.html.haml => _code.html.haml} (83%)
create mode 100644 app/views/projects/buttons/_download_menu_items.html.haml
create mode 100644 config/feature_flags/development/ci_retry_on_exit_codes.yml
create mode 100644 config/feature_flags/development/environments_folder_new_look.yml
delete mode 100644 db/fixtures/development/27_product_analytics_events.rb
create mode 100644 db/migrate/20231029142649_add_make_profile_private_application_setting.rb
create mode 100644 db/post_migrate/20231122100006_remove_custom_email_smtp_columns_from_service_desk_settings.rb
create mode 100644 db/schema_migrations/20231029142649
create mode 100644 db/schema_migrations/20231122100006
create mode 100644 doc/user/group/ssh_certificates.md
create mode 100644 lib/gitlab/circuit_breaker.rb
create mode 100644 lib/gitlab/circuit_breaker/notifier.rb
create mode 100644 lib/gitlab/circuit_breaker/store.rb
delete mode 100644 lib/product_analytics/event_params.rb
delete mode 100644 spec/factories/product_analytics_event.rb
create mode 100644 spec/features/environments/environments_folder_spec.rb
create mode 100644 spec/frontend/environments/folder/environments_folder_app_spec.js
create mode 100644 spec/lib/gitlab/circuit_breaker/notifier_spec.rb
create mode 100644 spec/lib/gitlab/circuit_breaker/store_spec.rb
create mode 100644 spec/lib/gitlab/circuit_breaker_spec.rb
delete mode 100644 spec/lib/product_analytics/event_params_spec.rb
delete mode 100644 spec/models/product_analytics_event_spec.rb
delete mode 100644 spec/services/product_analytics/build_activity_graph_service_spec.rb
delete mode 100644 spec/services/product_analytics/build_graph_service_spec.rb
diff --git a/.rubocop_todo/capybara/testid_finders.yml b/.rubocop_todo/capybara/testid_finders.yml
index f78470479e916..07bea3ea04f88 100644
--- a/.rubocop_todo/capybara/testid_finders.yml
+++ b/.rubocop_todo/capybara/testid_finders.yml
@@ -1,26 +1,11 @@
---
Capybara/TestidFinders:
Exclude:
- - 'spec/features/dashboard/projects_spec.rb'
- - 'spec/features/dashboard/todos/todos_spec.rb'
- - 'spec/features/groups/board_sidebar_spec.rb'
- - 'spec/features/groups/board_spec.rb'
- - 'spec/features/groups/clusters/user_spec.rb'
- - 'spec/features/groups/dependency_proxy_spec.rb'
- - 'spec/features/groups/group_settings_spec.rb'
- - 'spec/features/groups/members/leave_group_spec.rb'
- - 'spec/features/groups/members/manage_groups_spec.rb'
- - 'spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb'
- - 'spec/features/groups/members/search_members_spec.rb'
- - 'spec/features/groups/members/sort_members_spec.rb'
- - 'spec/features/groups/members/tabs_spec.rb'
- - 'spec/features/groups/merge_requests_spec.rb'
- 'spec/features/issues/issue_sidebar_spec.rb'
- 'spec/features/issues/issue_state_spec.rb'
- 'spec/features/issues/user_creates_issue_spec.rb'
- 'spec/features/issues/user_edits_issue_spec.rb'
- 'spec/features/issues/user_resets_their_incoming_email_token_spec.rb'
- - 'spec/features/issues/user_sees_live_update_spec.rb'
- 'spec/features/issues/user_sees_sidebar_updates_in_realtime_spec.rb'
- 'spec/features/issues/user_toggles_subscription_spec.rb'
- 'spec/features/labels_hierarchy_spec.rb'
@@ -38,7 +23,6 @@ Capybara/TestidFinders:
- 'spec/features/merge_request/user_merges_immediately_spec.rb'
- 'spec/features/merge_request/user_posts_notes_spec.rb'
- 'spec/features/merge_request/user_resolves_conflicts_spec.rb'
- - 'spec/features/merge_request/user_reverts_merge_request_spec.rb'
- 'spec/features/merge_request/user_sees_merge_widget_spec.rb'
- 'spec/features/merge_request/user_sees_pipelines_spec.rb'
- 'spec/features/merge_request/user_sees_suggest_pipeline_spec.rb'
@@ -47,7 +31,6 @@ Capybara/TestidFinders:
- 'spec/features/merge_request/user_toggles_whitespace_changes_spec.rb'
- 'spec/features/merge_request/user_views_open_merge_request_spec.rb'
- 'spec/features/milestone_spec.rb'
- - 'spec/features/nav/new_nav_callout_spec.rb'
- 'spec/features/nav/pinned_nav_items_spec.rb'
- 'spec/features/populate_new_pipeline_vars_with_params_spec.rb'
- 'spec/features/profile_spec.rb'
@@ -61,7 +44,6 @@ Capybara/TestidFinders:
- 'spec/features/profiles/user_edit_profile_spec.rb'
- 'spec/features/profiles/user_updates_comment_template_spec.rb'
- 'spec/features/project_group_variables_spec.rb'
- - 'spec/features/project_variables_spec.rb'
- 'spec/features/projects/blobs/blame_spec.rb'
- 'spec/features/projects/branches/user_deletes_branch_spec.rb'
- 'spec/features/projects/branches_spec.rb'
@@ -76,7 +58,6 @@ Capybara/TestidFinders:
- 'spec/features/projects/environments/environment_spec.rb'
- 'spec/features/projects/environments/environments_spec.rb'
- 'spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb'
- - 'spec/features/projects/features_visibility_spec.rb'
- 'spec/features/projects/fork_spec.rb'
- 'spec/features/projects/integrations/user_activates_jira_spec.rb'
- 'spec/features/projects/issues/design_management/user_views_designs_with_svg_xss_spec.rb'
@@ -111,7 +92,6 @@ Capybara/TestidFinders:
- 'spec/features/projects/terraform_spec.rb'
- 'spec/features/projects/tree/create_directory_spec.rb'
- 'spec/features/projects/tree/create_file_spec.rb'
- - 'spec/features/projects/user_uses_shortcuts_spec.rb'
- 'spec/features/projects/work_items/work_item_children_spec.rb'
- 'spec/features/projects/work_items/work_item_spec.rb'
- 'spec/features/protected_branches_spec.rb'
@@ -121,10 +101,8 @@ Capybara/TestidFinders:
- 'spec/features/search/user_searches_for_merge_requests_spec.rb'
- 'spec/features/search/user_searches_for_milestones_spec.rb'
- 'spec/features/search/user_searches_for_wiki_pages_spec.rb'
- - 'spec/features/search/user_uses_header_search_field_spec.rb'
- 'spec/features/search/user_uses_search_filters_spec.rb'
- 'spec/features/tags/developer_deletes_tag_spec.rb'
- 'spec/features/tags/maintainer_deletes_protected_tag_spec.rb'
- 'spec/features/triggers_spec.rb'
- - 'spec/features/uploads/user_uploads_avatar_to_profile_spec.rb'
- 'spec/features/user_sees_revert_modal_spec.rb'
diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml
index 8b59571249c21..ea2e52b028a97 100644
--- a/.rubocop_todo/gitlab/namespaced_class.yml
+++ b/.rubocop_todo/gitlab/namespaced_class.yml
@@ -250,7 +250,6 @@ Gitlab/NamespacedClass:
- 'app/models/plan.rb'
- 'app/models/plan_limits.rb'
- 'app/models/pool_repository.rb'
- - 'app/models/product_analytics_event.rb'
- 'app/models/programming_language.rb'
- 'app/models/project.rb'
- 'app/models/project_authorization.rb'
diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
index 6d996ff2f4ae9..b0ddd74796d09 100644
--- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml
+++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml
@@ -581,7 +581,6 @@ Layout/EmptyLineAfterMagicComment:
- 'spec/models/packages/rpm/repository_file_spec.rb'
- 'spec/models/packages/rubygems/metadatum_spec.rb'
- 'spec/models/packages/tag_spec.rb'
- - 'spec/models/product_analytics_event_spec.rb'
- 'spec/policies/design_management/design_policy_spec.rb'
- 'spec/requests/api/composer_packages_spec.rb'
- 'spec/requests/api/conan_project_packages_spec.rb'
diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml
index d7fb387b1a228..850672f4f6508 100644
--- a/.rubocop_todo/rspec/context_wording.yml
+++ b/.rubocop_todo/rspec/context_wording.yml
@@ -1994,7 +1994,6 @@ RSpec/ContextWording:
- 'spec/lib/object_storage/direct_upload_spec.rb'
- 'spec/lib/omni_auth/strategies/jwt_spec.rb'
- 'spec/lib/peek/views/bullet_detailed_spec.rb'
- - 'spec/lib/product_analytics/event_params_spec.rb'
- 'spec/lib/safe_zip/entry_spec.rb'
- 'spec/lib/security/ci_configuration/container_scanning_build_action_spec.rb'
- 'spec/lib/security/ci_configuration/sast_build_action_spec.rb'
diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml
index 174fda7107fe5..b41381c9afda0 100644
--- a/.rubocop_todo/rspec/feature_category.yml
+++ b/.rubocop_todo/rspec/feature_category.yml
@@ -4223,7 +4223,6 @@ RSpec/FeatureCategory:
- 'spec/lib/peek/views/external_http_spec.rb'
- 'spec/lib/peek/views/memory_spec.rb'
- 'spec/lib/peek/views/redis_detailed_spec.rb'
- - 'spec/lib/product_analytics/event_params_spec.rb'
- 'spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb'
- 'spec/lib/prometheus/pid_provider_spec.rb'
- 'spec/lib/quality/seeders/issues_spec.rb'
@@ -4709,7 +4708,6 @@ RSpec/FeatureCategory:
- 'spec/models/preloaders/project_root_ancestor_preloader_spec.rb'
- 'spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb'
- 'spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb'
- - 'spec/models/product_analytics_event_spec.rb'
- 'spec/models/programming_language_spec.rb'
- 'spec/models/project_authorization_spec.rb'
- 'spec/models/project_auto_devops_spec.rb'
diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml
index 230c292834313..80bb6ee92880c 100644
--- a/.rubocop_todo/rspec/named_subject.yml
+++ b/.rubocop_todo/rspec/named_subject.yml
@@ -473,7 +473,6 @@ RSpec/NamedSubject:
- 'ee/spec/lib/gitlab/llm/chat_message_spec.rb'
- 'ee/spec/lib/gitlab/llm/chat_storage_spec.rb'
- 'ee/spec/lib/gitlab/llm/completions/chat_spec.rb'
- - 'ee/spec/lib/gitlab/llm/concerns/circuit_breaker_spec.rb'
- 'ee/spec/lib/gitlab/llm/concerns/exponential_backoff_spec.rb'
- 'ee/spec/lib/gitlab/llm/graphql_subscription_response_service_spec.rb'
- 'ee/spec/lib/gitlab/llm/templates/categorize_question_spec.rb'
@@ -2503,7 +2502,6 @@ RSpec/NamedSubject:
- 'spec/lib/peek/views/external_http_spec.rb'
- 'spec/lib/peek/views/memory_spec.rb'
- 'spec/lib/peek/views/redis_detailed_spec.rb'
- - 'spec/lib/product_analytics/event_params_spec.rb'
- 'spec/lib/quality/seeders/issues_spec.rb'
- 'spec/lib/release_highlights/validator/entry_spec.rb'
- 'spec/lib/release_highlights/validator_spec.rb'
@@ -3437,8 +3435,6 @@ RSpec/NamedSubject:
- 'spec/services/personal_access_tokens/last_used_service_spec.rb'
- 'spec/services/personal_access_tokens/revoke_service_spec.rb'
- 'spec/services/post_receive_service_spec.rb'
- - 'spec/services/product_analytics/build_activity_graph_service_spec.rb'
- - 'spec/services/product_analytics/build_graph_service_spec.rb'
- 'spec/services/projects/alerting/notify_service_spec.rb'
- 'spec/services/projects/all_issues_count_service_spec.rb'
- 'spec/services/projects/all_merge_requests_count_service_spec.rb'
diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml
index e49171b0362e0..a9c02aa579645 100644
--- a/.rubocop_todo/style/inline_disable_annotation.yml
+++ b/.rubocop_todo/style/inline_disable_annotation.yml
@@ -2081,7 +2081,6 @@ Style/InlineDisableAnnotation:
- 'ee/spec/lib/gitlab/import_export/project/relation_factory_spec.rb'
- 'ee/spec/lib/gitlab/llm/chain/agents/zero_shot/executor_real_requests_spec.rb'
- 'ee/spec/lib/gitlab/llm/chain/tools/epic_identifier/executor_spec.rb'
- - 'ee/spec/lib/gitlab/llm/concerns/circuit_breaker_spec.rb'
- 'ee/spec/lib/gitlab/mirror_spec.rb'
- 'ee/spec/lib/gitlab/patch/database_config_spec.rb'
- 'ee/spec/lib/gitlab/sitemaps/sitemap_file_spec.rb'
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
index 64229f5490418..3a9ec34178968 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -2,6 +2,7 @@
import { GlBanner, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
+import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
const defaultTitle = __('CI/CD Catalog');
@@ -11,6 +12,7 @@ const defaultDescription = s__(
export default {
components: {
+ BetaBadge,
GlBanner,
GlLink,
},
@@ -58,7 +60,10 @@ export default {
{{ $options.i18n.banner.description }}
- {{ pageTitle }}
+
+
{{ pageTitle }}
+
+
{{ pageDescription }}
{{
diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js
index 79280c13f0f4c..d2a5d5a9db7c1 100644
--- a/app/assets/javascripts/clone_panel.js
+++ b/app/assets/javascripts/clone_panel.js
@@ -14,7 +14,7 @@ export default function initClonePanel() {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
- $('a', $cloneOptions).on('click', (e) => {
+ $('.js-clone-links a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
if (
diff --git a/app/assets/javascripts/environments/folder/environments_folder_app.vue b/app/assets/javascripts/environments/folder/environments_folder_app.vue
new file mode 100644
index 0000000000000..a963ca9b14405
--- /dev/null
+++ b/app/assets/javascripts/environments/folder/environments_folder_app.vue
@@ -0,0 +1,21 @@
+
+
+
+ {{ $options.i18n.pageTitle }} /
+ {{ folderName }}
+
+
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 1a32de30de006..beaf8041c39ee 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -2,7 +2,8 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import Translate from '~/vue_shared/translate';
-import EnvironmentsFolderApp from './environments_folder_view.vue';
+import EnvironmentsFolderView from './environments_folder_view.vue';
+import EnvironmentsFolderApp from './environments_folder_app.vue';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -13,19 +14,35 @@ const apolloProvider = new VueApollo({
export default () => {
const el = document.getElementById('environments-folder-list-view');
+ const environmentsData = el.dataset;
+ if (gon.features.environmentsFolderNewLook) {
+ const folderName = environmentsData.environmentsDataFolderName;
+
+ return new Vue({
+ el,
+ components: {
+ EnvironmentsFolderApp,
+ },
+ render(createElement) {
+ return createElement('environments-folder-app', {
+ props: {
+ folderName,
+ },
+ });
+ },
+ });
+ }
return new Vue({
el,
components: {
- EnvironmentsFolderApp,
+ EnvironmentsFolderView,
},
apolloProvider,
provide: {
projectPath: el.dataset.projectPath,
},
data() {
- const environmentsData = el.dataset;
-
return {
endpoint: environmentsData.environmentsDataEndpoint,
folderName: environmentsData.environmentsDataFolderName,
@@ -33,7 +50,7 @@ export default () => {
};
},
render(createElement) {
- return createElement('environments-folder-app', {
+ return createElement('environments-folder-view', {
props: {
endpoint: this.endpoint,
folderName: this.folderName,
diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
index 9111dc9c86cac..eab25298c3648 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
@@ -10,6 +10,7 @@ import produce from 'immer';
import {
getK8sPods,
handleClusterError,
+ buildWatchPath,
} from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers';
import { humanizeClusterErrors } from '../../helpers/k8s_integration_helper';
import k8sPodsQuery from '../queries/k8s_pods.query.graphql';
@@ -62,9 +63,7 @@ const mapWorkloadItems = (items, kind) => {
const watchWorkloadItems = ({ kind, apiVersion, configuration, namespace, client }) => {
const itemKind = kind.toLowerCase().replace('list', 's');
- const path = namespace
- ? `/apis/${apiVersion}/namespaces/${namespace}/${itemKind}`
- : `/apis/${apiVersion}/${itemKind}`;
+ const path = buildWatchPath({ resource: itemKind, api: `apis/${apiVersion}`, namespace });
const config = new Configuration(configuration);
const watcherApi = new WatchApi(config);
@@ -113,7 +112,7 @@ const mapServicesItems = (items) => {
};
const watchServices = ({ configuration, namespace, client }) => {
- const path = namespace ? `/api/v1/namespaces/${namespace}/services` : '/api/v1/services';
+ const path = buildWatchPath({ resource: 'services', namespace });
const config = new Configuration(configuration);
const watcherApi = new WatchApi(config);
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 39a8b1d0a9cd3..1babccff4253e 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -262,6 +262,38 @@ class GfmAutoComplete {
});
}
+ // eslint-disable-next-line class-methods-use-this
+ setSubmitReviewStates($input) {
+ if (!window.gon.features?.mrRequestChanges) return;
+
+ const REVIEW_STATES = {
+ reviewed: {
+ header: __('Comment'),
+ description: __('Submit general feedback without explicit approval.'),
+ },
+ approve: {
+ header: __('Approve'),
+ description: __('Submit feedback and approve these changes.'),
+ },
+ requested_changes: {
+ header: __('Request changes'),
+ description: __('Submit feedback that should be addressed before merging.'),
+ },
+ };
+
+ $input.filter('[data-supports-quick-actions="true"]').atwho({
+ // Always keep the trailing space otherwise the command won't display correctly
+ at: '/submit_review ',
+ alias: 'submit_review',
+ data: Object.keys(REVIEW_STATES),
+ displayTpl({ name }) {
+ const reviewState = REVIEW_STATES[name];
+
+ return `${reviewState.header}${reviewState.description}`;
+ },
+ });
+ }
+
setupEmoji($input) {
const fetchData = this.fetchData.bind(this);
@@ -851,6 +883,9 @@ class GfmAutoComplete {
} else if (dataSource) {
AjaxCache.retrieve(dataSource, true)
.then((data) => {
+ if (data.some((c) => c.name === 'submit_review')) {
+ this.setSubmitReviewStates($input);
+ }
this.loadData($input, at, data);
})
.catch(() => {
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
index 22ddd7c6a6100..4eec31cfe3725 100644
--- a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
@@ -9,8 +9,12 @@ export const handleClusterError = async (err) => {
throw errorData;
};
+export const buildWatchPath = ({ resource, api = 'api/v1', namespace = '' }) => {
+ return namespace ? `/${api}/namespaces/${namespace}/${resource}` : `/${api}/${resource}`;
+};
+
export const watchPods = ({ client, query, configuration, namespace }) => {
- const path = namespace ? `/api/v1/namespaces/${namespace}/pods` : '/api/v1/pods';
+ const path = buildWatchPath({ resource: 'pods', namespace });
const config = new Configuration(configuration);
const watcherApi = new WatchApi(config);
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
index 23b2ae74e41c1..be9c768ae6043 100644
--- a/app/assets/javascripts/notes/components/notes_activity_header.vue
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -38,9 +38,7 @@ export default {
},
computed: {
showAiActions() {
- return (
- this.resourceGlobalId && this.glFeatures.aiGlobalSwitch && this.glFeatures.summarizeNotes
- );
+ return this.resourceGlobalId && this.glFeatures.summarizeNotes;
},
},
};
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index e0c49412d5654..307be0b0aa025 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -67,10 +67,7 @@ export default {
return this.currentScope === SCOPE_MILESTONES;
},
showWikiBlobsFilters() {
- return (
- this.currentScope === SCOPE_WIKI_BLOBS &&
- this.glFeatures?.searchProjectWikisHideArchivedProjects
- );
+ return this.currentScope === SCOPE_WIKI_BLOBS;
},
},
methods: {
diff --git a/app/assets/stylesheets/page_bundles/project.scss b/app/assets/stylesheets/page_bundles/project.scss
index 8d8da10268a6d..504f14051482d 100644
--- a/app/assets/stylesheets/page_bundles/project.scss
+++ b/app/assets/stylesheets/page_bundles/project.scss
@@ -53,7 +53,7 @@
}
}
- .project-clone-holder {
+ .project-code-holder {
display: inline-block;
margin: $gl-padding 0 0;
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 4b2749dc7169b..8cdd6efa7c565 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -14,6 +14,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:k8s_watch_api, project)
end
+ before_action only: [:folder] do
+ push_frontend_feature_flag(:environments_folder_new_look, project)
+ end
+
before_action :authorize_read_environment!
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
diff --git a/app/models/product_analytics_event.rb b/app/models/product_analytics_event.rb
deleted file mode 100644
index 52baa3be6c448..0000000000000
--- a/app/models/product_analytics_event.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-class ProductAnalyticsEvent < ApplicationRecord
- self.table_name = 'product_analytics_events_experimental'
-
- # Ignore that the partition key :project_id is part of the formal primary key
- self.primary_key = :id
-
- belongs_to :project
-
- validates :event_id, :project_id, :v_collector, :v_etl, presence: true
-
- # There is no default Rails timestamps in the table.
- # collector_tstamp is a timestamp when a collector recorded an event.
- scope :order_by_time, -> { order(collector_tstamp: :desc) }
-
- # If we decide to change this scope to use date_trunc('day', collector_tstamp),
- # we should remember that a btree index on collector_tstamp will be no longer effective.
- scope :timerange, ->(duration, today = Time.zone.today) {
- where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1)
- }
-
- def self.count_by_graph(graph, days)
- group(graph).timerange(days).count
- end
-
- def self.count_collector_tstamp_by_day(days)
- group("DATE_TRUNC('day', collector_tstamp)")
- .reorder('date_trunc_day_collector_tstamp')
- .timerange(days)
- .count
- end
-
- def as_json_wo_empty
- as_json.compact
- end
-end
diff --git a/app/models/project.rb b/app/models/project.rb
index 57c127965f76c..5788885498cb0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -468,10 +468,6 @@ def self.integration_association_name(name)
# rubocop:enable Cop/ActiveRecordDependent
has_many :active_pages_deployments, -> { active }, class_name: 'PagesDeployment', inverse_of: :project
- # Can be too many records. We need to implement delete_all in batches.
- # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/228637
- has_many :product_analytics_events, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
has_many :operations_feature_flags, class_name: 'Operations::FeatureFlag'
has_one :operations_feature_flags_client, class_name: 'Operations::FeatureFlagsClient'
has_many :operations_feature_flags_user_lists, class_name: 'Operations::FeatureFlags::UserList'
diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb
index 04fbc8467c9ef..ccab3d9f02d5d 100644
--- a/app/policies/user_policy.rb
+++ b/app/policies/user_policy.rb
@@ -33,6 +33,7 @@ class UserPolicy < BasePolicy
enable :read_saved_replies
enable :read_user_email_address
enable :admin_user_email_address
+ enable :make_profile_private
end
rule { default }.enable :read_user_profile
diff --git a/app/services/product_analytics/build_activity_graph_service.rb b/app/services/product_analytics/build_activity_graph_service.rb
deleted file mode 100644
index 63108d76afd99..0000000000000
--- a/app/services/product_analytics/build_activity_graph_service.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module ProductAnalytics
- class BuildActivityGraphService < BuildGraphService
- def execute
- timerange = @params[:timerange].days
-
- results = product_analytics_events.count_collector_tstamp_by_day(timerange)
-
- format_results('collector_tstamp', results.transform_keys(&:to_date))
- end
- end
-end
diff --git a/app/services/product_analytics/build_graph_service.rb b/app/services/product_analytics/build_graph_service.rb
deleted file mode 100644
index da54ad4de0eb7..0000000000000
--- a/app/services/product_analytics/build_graph_service.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-module ProductAnalytics
- class BuildGraphService
- def initialize(project, params)
- @project = project
- @params = params
- end
-
- def execute
- graph = @params[:graph].to_sym
- timerange = @params[:timerange].days
-
- results = product_analytics_events.count_by_graph(graph, timerange)
-
- format_results(graph, results)
- end
-
- private
-
- def format_results(name, results)
- {
- id: name,
- keys: results.keys,
- values: results.values
- }
- end
-
- def product_analytics_events
- @project.product_analytics_events
- end
- end
-end
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 00da6c73081cf..68e73a017a740 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -5,8 +5,8 @@
.controls.gl-display-flex
= link_button_to nil, project_path(@project, rss_url_options), title: s_("ProjectActivityRSS|Subscribe"), class: 'd-none d-sm-inline-flex has-tooltip', icon: 'rss'
- if is_project_overview && can?(current_user, :download_code, @project)
- .project-clone-holder.d-none.d-md-inline-flex.gl-ml-2
- = render "projects/buttons/clone", dropdown_class: 'dropdown-menu-right'
+ .project-code-holder.d-none.d-md-inline-flex.gl-ml-2
+ = render "projects/buttons/code", dropdown_class: 'dropdown-menu-right', ref: @ref
.content_list.project-activity{ :"data-href" => activity_project_path(@project) }
.loading
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_code.html.haml
similarity index 83%
rename from app/views/projects/buttons/_clone.html.haml
rename to app/views/projects/buttons/_code.html.haml
index 0e645eda678c1..0b2a527025edb 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_code.html.haml
@@ -1,15 +1,16 @@
- project = project || @project
- dropdown_class = local_assigns.fetch(:dropdown_class, '')
+- ref = local_assigns.fetch(:ref)
- if can?(current_user, :download_code, @project)
.git-clone-holder.js-git-clone-holder
= render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { id: 'clone-dropdown', class: 'clone-dropdown-btn', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } }) do
- %span.gl-mr-2.js-clone-dropdown-label
- = _('Clone')
+ %span.js-clone-dropdown-label
+ = _('Code')
= sprite_icon("chevron-down", css_class: "icon")
- %ul.dropdown-menu.dropdown-menu-large.dropdown-menu-selectable.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } }
+ %ul.dropdown-menu.dropdown-menu-large.clone-options-dropdown{ class: dropdown_class, data: { qa_selector: 'clone_dropdown_content' } }
- if ssh_enabled?
- %li{ class: 'gl-px-4!' }
+ %li.gl-dropdown-item.js-clone-links{ class: 'gl-px-4!' }
%label.label-bold
= _('Clone with SSH')
.input-group.btn-group
@@ -18,7 +19,7 @@
= clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), category: :primary, size: :medium)
= render_if_exists 'projects/buttons/geo'
- if http_enabled?
- %li.pt-2{ class: 'gl-px-4!' }
+ %li.pt-2.gl-dropdown-item.js-clone-links{ class: 'gl-px-4!' }
%label.label-bold
= _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
.input-group.btn-group
@@ -28,7 +29,7 @@
= render_if_exists 'projects/buttons/geo'
= render_if_exists 'projects/buttons/kerberos_clone_field'
%li.divider.mt-2
- %li.pt-2.gl-dropdown-item
+ %li.pt-2.gl-dropdown-item.js-clone-links
%label.label-bold{ class: 'gl-px-4!' }
= _('Open in your IDE')
- if ssh_enabled?
@@ -53,3 +54,6 @@
%a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) }
.gl-dropdown-item-text-wrapper
= _("Xcode")
+ - if !project.empty_repo? && can?(current_user, :download_code, project)
+ %li.divider.mt-2
+ = render 'projects/buttons/download_menu_items', project: project, ref: ref
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index b3282742407f6..946057a2e2fe0 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,27 +1,13 @@
- project = local_assigns.fetch(:project)
- ref = local_assigns.fetch(:ref)
-- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) }
+- pipeline = local_assigns.fetch(:pipeline, nil)
- css_class = local_assigns.fetch(:css_class, '')
- if !project.empty_repo? && can?(current_user, :download_code, project)
- - archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
.project-action-button.dropdown.gl-dropdown.inline{ class: css_class }>
= render Pajamas::ButtonComponent.new(button_options: { class: 'dropdown-toggle gl-dropdown-toggle dropdown-icon-only has-tooltip', title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static', data: { testid: 'download-source-code-button' } }) do
= sprite_icon('download', css_class: 'gl-icon dropdown-icon')
%span.sr-only= _('Select Archive Format')
= sprite_icon('chevron-down', css_class: 'gl-icon dropdown-chevron')
- .dropdown-menu.dropdown-menu-right{ role: 'menu' }
- %section
- %h5.m-0.dropdown-bold-header= _('Download source code')
- .dropdown-menu-content
- = render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: nil
- .js-directory-downloads{ data: { links: directory_download_links(project, ref, archive_prefix).to_json } }
- - if pipeline && pipeline.latest_builds_with_artifacts.any?
- %section.border-top.pt-1.mt-1
- %h5.m-0.dropdown-bold-header= _('Download artifacts')
- - unless pipeline.latest?
- %span.unclickable= ci_status_for_statuseable(project.latest_pipeline(ref))
- %h6.m-0.dropdown-header= _('Previous Artifacts')
- %ul
- - pipeline.latest_builds_with_artifacts.each do |job|
- %li= link_to job.name, latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: ''
+ %ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
+ = render 'projects/buttons/download_menu_items', project: project, ref: ref, pipeline: pipeline
diff --git a/app/views/projects/buttons/_download_links.html.haml b/app/views/projects/buttons/_download_links.html.haml
index 31185fc153215..7035f3b37927f 100644
--- a/app/views/projects/buttons/_download_links.html.haml
+++ b/app/views/projects/buttons/_download_links.html.haml
@@ -1,4 +1,5 @@
-.btn-group.ml-0.w-100
- - Gitlab::Workhorse::ARCHIVE_FORMATS.each_with_index do |fmt, index|
- - archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
- = link_button_to fmt, external_storage_url_or_path(archive_path), rel: 'nofollow', download: '', variant: index == 0 ? :confirm : :default, size: :small
+- Gitlab::Workhorse::ARCHIVE_FORMATS.each_with_index do |fmt, index|
+ - archive_path = project_archive_path(project, id: tree_join(ref, archive_prefix), path: path, format: fmt)
+
+ %a.dropdown-item.open-with-link{ href: external_storage_url_or_path(archive_path), rel: 'nofollow', download: '' }
+ .gl-dropdown-item-text-wrapper= fmt
diff --git a/app/views/projects/buttons/_download_menu_items.html.haml b/app/views/projects/buttons/_download_menu_items.html.haml
new file mode 100644
index 0000000000000..e4fcae1815c85
--- /dev/null
+++ b/app/views/projects/buttons/_download_menu_items.html.haml
@@ -0,0 +1,21 @@
+- project = local_assigns.fetch(:project)
+- ref = local_assigns.fetch(:ref)
+- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) }
+- archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
+
+%li.gl-dropdown-item
+ %h5.m-0.dropdown-bold-header= _('Download source code')
+ = render 'projects/buttons/download_links', project: project, ref: ref, archive_prefix: archive_prefix, path: nil
+.js-directory-downloads{ data: { links: directory_download_links(project, ref, archive_prefix).to_json } }
+- if pipeline && pipeline.latest_builds_with_artifacts.any?
+ %li.divider.mt-2
+ %li.gl-dropdown-item
+ %h5.m-0.dropdown-bold-header= _('Download artifacts')
+ - unless pipeline.latest?
+ %span.gl-ml-3.unclickable= ci_status_for_statuseable(project.latest_pipeline(ref))
+ %li.divider.mt-2
+ %li.gl-dropdown-item
+ %h5.m-0.dropdown-bold-header= _('Previous Artifacts')
+ - pipeline.latest_builds_with_artifacts.each do |job|
+ = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), class: 'dropdown-item open-with-link', rel: 'nofollow', download: '' do
+ .gl-dropdown-item-text-wrapper= job.name
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 60b5ab376ecaa..ca9900a646e4d 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -19,11 +19,11 @@
= _('You can get started by cloning the repository or start adding files to it with one of the following options.')
.project-buttons{ data: { testid: 'quick-actions-container' } }
- .project-clone-holder.d-block.d-md-none.gl-mt-3.gl-mr-3
+ .project-code-holder.d-block.d-md-none.gl-mt-3.gl-mr-3
= render "shared/mobile_clone_panel"
- .project-clone-holder.d-none.d-md-inline-block.gl-mb-3.gl-mr-3.float-left
- = render "projects/buttons/clone"
+ .project-code-holder.d-none.d-md-inline-block.gl-mb-3.gl-mr-3.float-left
+ = render "projects/buttons/code", ref: @ref
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons, project_buttons: true
- if can?(current_user, :push_code, @project)
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index bed37d9cb63da..8eb0bb85e660d 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -16,10 +16,10 @@
= render 'projects/find_file_link'
= render 'shared/web_ide_button', blob: nil
- = render 'projects/buttons/download', project: @project, ref: @ref
- .project-clone-holder.d-none.d-md-inline-block>
- = render "projects/buttons/clone", dropdown_class: 'dropdown-menu-right'
+ .project-code-holder.d-none.d-md-inline-block>
+ = render "projects/buttons/code", dropdown_class: 'dropdown-menu-right', ref: @ref
- .project-clone-holder.d-block.d-md-none.mt-sm-2.mt-md-0.ml-md-2>
- = render "shared/mobile_clone_panel"
+ .project-code-holder.d-block.d-md-none.mt-sm-2.mt-md-0.ml-md-2>
+ = render 'projects/buttons/download', project: @project, ref: @ref
+ = render "shared/mobile_clone_panel", ref: @ref
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 4b39ec5283757..7dce8737eb490 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -10,9 +10,9 @@
= default_clone_protocol.upcase
= sprite_icon('chevron-down', css_class: 'gl-icon')
%ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown{ data: { testid: 'clone-dropdown-content' } }
- %li
+ %li.js-clone-links
= ssh_clone_button(container)
- %li
+ %li.js-clone-links
= http_clone_button(container)
= render_if_exists 'shared/kerberos_clone_button', container: container
diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml
index c594cee326ebb..2f7fb348c17db 100644
--- a/app/views/shared/_mobile_clone_panel.html.haml
+++ b/app/views/shared/_mobile_clone_panel.html.haml
@@ -8,9 +8,9 @@
= sprite_icon("chevron-down", css_class: "dropdown-btn-icon icon")
%ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } }
- if ssh_enabled?
- %li
+ %li.js-clone-links
= dropdown_item_with_description(ssh_copy_label, ssh_clone_url_to_repo(project), href: ssh_clone_url_to_repo(project), data: { clone_type: 'ssh' }, default: true)
- if http_enabled?
- %li
+ %li.js-clone-links
= dropdown_item_with_description(http_copy_label, http_clone_url_to_repo(project), href: http_clone_url_to_repo(project), data: { clone_type: 'http' })
= render_if_exists 'shared/mobile_kerberos_clone'
diff --git a/config/feature_flags/development/ci_retry_on_exit_codes.yml b/config/feature_flags/development/ci_retry_on_exit_codes.yml
new file mode 100644
index 0000000000000..eb5f4abb618ca
--- /dev/null
+++ b/config/feature_flags/development/ci_retry_on_exit_codes.yml
@@ -0,0 +1,8 @@
+---
+name: ci_retry_on_exit_codes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135430
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430037
+milestone: '16.7'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/environments_folder_new_look.yml b/config/feature_flags/development/environments_folder_new_look.yml
new file mode 100644
index 0000000000000..218ea2cf44842
--- /dev/null
+++ b/config/feature_flags/development/environments_folder_new_look.yml
@@ -0,0 +1,8 @@
+---
+name: environments_folder_new_look
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137046
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/431928
+milestone: '16.7'
+type: development
+group: group::environments
+default_enabled: false
diff --git a/config/initializers/circuitbox.rb b/config/initializers/circuitbox.rb
index 966320265aa5e..55c06cf868b91 100644
--- a/config/initializers/circuitbox.rb
+++ b/config/initializers/circuitbox.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-Gitlab.ee do
- Circuitbox.configure do |config|
- config.default_circuit_store = Gitlab::CircuitBreaker::Store.new
- config.default_notifier = Gitlab::CircuitBreaker::Notifier.new
- end
+Circuitbox.configure do |config|
+ config.default_circuit_store = Gitlab::CircuitBreaker::Store.new
+ config.default_notifier = Gitlab::CircuitBreaker::Notifier.new
end
diff --git a/db/fixtures/development/27_product_analytics_events.rb b/db/fixtures/development/27_product_analytics_events.rb
deleted file mode 100644
index 19237afd8eab3..0000000000000
--- a/db/fixtures/development/27_product_analytics_events.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-Gitlab::Seeder.quiet do
- # The data set takes approximately 2 minutes to load,
- # so its put behind the flag. To seed this data use the flag and the filter:
- # SEED_PRODUCT_ANALYTICS_EVENTS=1 FILTER=product_analytics_events rake db:seed_fu
- flag = 'SEED_PRODUCT_ANALYTICS_EVENTS'
-
- if ENV[flag]
- Project.all.sample(2).each do |project|
- # Let's generate approx a week of events from now into the past with 1 minute step.
- # To add some differentiation we add a random offset of up to 45 seconds.
- 10000.times do |i|
- dvce_created_tstamp = DateTime.now - i.minute - rand(45).seconds
-
- # Add a random delay to collector timestamp. Up to 2 seconds.
- collector_tstamp = dvce_created_tstamp + rand(3).second
-
- ProductAnalyticsEvent.create!(
- project_id: project.id,
- platform: ["web", "mob", "mob", "app"].sample,
- collector_tstamp: collector_tstamp,
- dvce_created_tstamp: dvce_created_tstamp,
- event: nil,
- event_id: SecureRandom.uuid,
- name_tracker: "sp",
- v_tracker: "js-2.14.0",
- v_collector: Gitlab::VERSION,
- v_etl: Gitlab::VERSION,
- domain_userid: SecureRandom.uuid,
- domain_sessionidx: 4,
- page_url: "#{project.web_url}/-/product_analytics/test",
- page_title: 'Test page',
- page_referrer: "#{project.web_url}/-/product_analytics/test",
- br_lang: ["en-US", "en-US", "en-GB", "nl", "fi"].sample, # https://www.andiamo.co.uk/resources/iso-language-codes/
- br_features_pdf: true,
- br_cookies: [true, true, true, false].sample,
- br_colordepth: ["24", "24", "16", "8"].sample,
- os_timezone: ["America/Los_Angeles", "America/Los_Angeles", "America/Lima", "Asia/Dubai", "Africa/Bangui"].sample, # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
- doc_charset: ["UTF-8", "UTF-8", "UTF-8", "DOS", "EUC"].sample,
- domain_sessionid: SecureRandom.uuid
- )
- end
-
- unless Feature.enabled?(:product_analytics, project)
- if Feature.enable(:product_analytics, project)
- puts "Product analytics feature was enabled for #{project.full_path}"
- end
- end
-
- puts "10K events added to #{project.full_path}"
- end
- else
- puts "Skipped. Use the `#{flag}` environment variable to enable."
- end
-end
diff --git a/db/migrate/20231029142649_add_make_profile_private_application_setting.rb b/db/migrate/20231029142649_add_make_profile_private_application_setting.rb
new file mode 100644
index 0000000000000..ff946d56e7201
--- /dev/null
+++ b/db/migrate/20231029142649_add_make_profile_private_application_setting.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddMakeProfilePrivateApplicationSetting < Gitlab::Database::Migration[2.2]
+ milestone '16.7'
+
+ def change
+ add_column(:application_settings, :make_profile_private, :boolean, default: true, null: false)
+ end
+end
diff --git a/db/post_migrate/20231122100006_remove_custom_email_smtp_columns_from_service_desk_settings.rb b/db/post_migrate/20231122100006_remove_custom_email_smtp_columns_from_service_desk_settings.rb
new file mode 100644
index 0000000000000..98bb15dd76e79
--- /dev/null
+++ b/db/post_migrate/20231122100006_remove_custom_email_smtp_columns_from_service_desk_settings.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class RemoveCustomEmailSmtpColumnsFromServiceDeskSettings < Gitlab::Database::Migration[2.2]
+ MAXIMUM_LIMIT = 255
+
+ milestone '16.7'
+
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ remove_column :service_desk_settings, :custom_email_smtp_address
+ remove_column :service_desk_settings, :custom_email_smtp_port
+ remove_column :service_desk_settings, :custom_email_smtp_username
+ remove_column :service_desk_settings, :encrypted_custom_email_smtp_password
+ remove_column :service_desk_settings, :encrypted_custom_email_smtp_password_iv
+ end
+ end
+
+ def down
+ with_lock_retries do
+ add_column :service_desk_settings, :custom_email_smtp_address, :text
+ add_column :service_desk_settings, :custom_email_smtp_port, :integer
+ add_column :service_desk_settings, :custom_email_smtp_username, :text
+ add_column :service_desk_settings, :encrypted_custom_email_smtp_password, :binary
+ add_column :service_desk_settings, :encrypted_custom_email_smtp_password_iv, :binary
+ end
+
+ add_text_limit :service_desk_settings, :custom_email_smtp_address, MAXIMUM_LIMIT
+ add_text_limit :service_desk_settings, :custom_email_smtp_username, MAXIMUM_LIMIT
+ end
+end
diff --git a/db/schema_migrations/20231029142649 b/db/schema_migrations/20231029142649
new file mode 100644
index 0000000000000..8e9924fb32e9a
--- /dev/null
+++ b/db/schema_migrations/20231029142649
@@ -0,0 +1 @@
+6b8021c293e630af13479afe8f49ef3d28861d963942b481a3113266ff59fccf
\ No newline at end of file
diff --git a/db/schema_migrations/20231122100006 b/db/schema_migrations/20231122100006
new file mode 100644
index 0000000000000..5cec39e6f6031
--- /dev/null
+++ b/db/schema_migrations/20231122100006
@@ -0,0 +1 @@
+cf9a4cbefa65c11d5066134ff82615453aaf63af3f6f871d532038439ada6d22
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index b915344d7cb0b..0fd47d9b0d4e9 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -12196,6 +12196,7 @@ CREATE TABLE application_settings (
project_jobs_api_rate_limit integer DEFAULT 600 NOT NULL,
math_rendering_limits_enabled boolean DEFAULT true NOT NULL,
service_access_tokens_expiration_enforced boolean DEFAULT true NOT NULL,
+ make_profile_private boolean DEFAULT true NOT NULL,
enable_artifact_external_redirect_warning_page boolean DEFAULT true NOT NULL,
allow_project_creation_for_guest_and_below boolean DEFAULT true NOT NULL,
update_namespace_name_rate_limit smallint DEFAULT 120 NOT NULL,
@@ -23437,16 +23438,9 @@ CREATE TABLE service_desk_settings (
file_template_project_id bigint,
custom_email_enabled boolean DEFAULT false NOT NULL,
custom_email text,
- custom_email_smtp_address text,
- custom_email_smtp_port integer,
- custom_email_smtp_username text,
- encrypted_custom_email_smtp_password bytea,
- encrypted_custom_email_smtp_password_iv bytea,
service_desk_enabled boolean DEFAULT true NOT NULL,
add_external_participants_from_cc boolean DEFAULT false NOT NULL,
- CONSTRAINT check_57a79552e1 CHECK ((char_length(custom_email) <= 255)),
- CONSTRAINT check_b283637a9e CHECK ((char_length(custom_email_smtp_address) <= 255)),
- CONSTRAINT check_e3535d46ee CHECK ((char_length(custom_email_smtp_username) <= 255))
+ CONSTRAINT check_57a79552e1 CHECK ((char_length(custom_email) <= 255))
);
CREATE TABLE shards (
diff --git a/doc/administration/logs/index.md b/doc/administration/logs/index.md
index b32d19f174fd7..b61970b6fa522 100644
--- a/doc/administration/logs/index.md
+++ b/doc/administration/logs/index.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/administration/monitoring/health_check.md b/doc/administration/monitoring/health_check.md
index 4dbbdf6f3c9dc..029143564d1e3 100644
--- a/doc/administration/monitoring/health_check.md
+++ b/doc/administration/monitoring/health_check.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/administration/monitoring/index.md b/doc/administration/monitoring/index.md
index 622a772a63894..ef4d40a7fca8c 100644
--- a/doc/administration/monitoring/index.md
+++ b/doc/administration/monitoring/index.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/administration/monitoring/performance/gitlab_configuration.md b/doc/administration/monitoring/performance/gitlab_configuration.md
index a32a38a11e564..858c94f3671a8 100644
--- a/doc/administration/monitoring/performance/gitlab_configuration.md
+++ b/doc/administration/monitoring/performance/gitlab_configuration.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/administration/monitoring/performance/grafana_configuration.md b/doc/administration/monitoring/performance/grafana_configuration.md
index a69eb67e77e78..15ccc2094c82f 100644
--- a/doc/administration/monitoring/performance/grafana_configuration.md
+++ b/doc/administration/monitoring/performance/grafana_configuration.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index b632108b10353..f965288611c5c 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/administration/settings/incident_management_rate_limits.md b/doc/administration/settings/incident_management_rate_limits.md
index 5a8b1b96c6d79..ef646b5e219a7 100644
--- a/doc/administration/settings/incident_management_rate_limits.md
+++ b/doc/administration/settings/incident_management_rate_limits.md
@@ -1,6 +1,6 @@
---
type: reference
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/api/alert_management_alerts.md b/doc/api/alert_management_alerts.md
index 5da77d0860543..049754d28878a 100644
--- a/doc/api/alert_management_alerts.md
+++ b/doc/api/alert_management_alerts.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/api/group_ssh_certificates.md b/doc/api/group_ssh_certificates.md
index 3d99bdcaa3a91..d906bbefa8e45 100644
--- a/doc/api/group_ssh_certificates.md
+++ b/doc/api/group_ssh_certificates.md
@@ -4,12 +4,11 @@ group: Source Code
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
---
-# Group SSH certificates API **(PREMIUM ALL)**
+# Group SSH certificates API **(PREMIUM SAAS)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421915) in GitLab 16.4 [with a flag](../user/feature_flags.md) named `ssh_certificates_rest_endpoints`. Disabled by default.
FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available, an administrator can [enable the feature flag](../administration/feature_flags.md) named `ssh_certificates_rest_endpoints`.
On GitLab.com, this feature is not available.
Use this API to create, read and delete SSH certificates for a group.
diff --git a/doc/api/metrics_dashboard_annotations.md b/doc/api/metrics_dashboard_annotations.md
index 37d19860e6a42..56bd1066e2e6a 100644
--- a/doc/api/metrics_dashboard_annotations.md
+++ b/doc/api/metrics_dashboard_annotations.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
type: concepts, howto
diff --git a/doc/api/metrics_user_starred_dashboards.md b/doc/api/metrics_user_starred_dashboards.md
index 6e839e75ae946..2c358e080c200 100644
--- a/doc/api/metrics_user_starred_dashboards.md
+++ b/doc/api/metrics_user_starred_dashboards.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
type: concepts, howto
diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md
index b5b049346b6af..86732c3a8acc0 100644
--- a/doc/development/distributed_tracing.md
+++ b/doc/development/distributed_tracing.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
diff --git a/doc/development/logging.md b/doc/development/logging.md
index cd4b9f2cd8c6a..123c5c46f9210 100644
--- a/doc/development/logging.md
+++ b/doc/development/logging.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 681f345fb8687..25d48632bd656 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -1019,6 +1019,10 @@ end
## Dropping a database table
+NOTE:
+After a table has been dropped, it should be added to the database dictionary, following the
+steps in the [database dictionary guide](database/database_dictionary.md#dropping-tables).
+
Dropping a database table is uncommon, and the `drop_table` method
provided by Rails is generally considered safe. Before dropping the table,
please consider the following:
@@ -1105,9 +1109,6 @@ class DroppingTableMigrationClass < Gitlab::Database::Migration[2.1]
end
```
-After a table has been dropped, it should be added to the database dictionary, following the
-steps in the [database dictionary guide](database/database_dictionary.md#dropping-tables).
-
## Dropping a sequence
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88387) in GitLab 15.1.
diff --git a/doc/development/prometheus_metrics.md b/doc/development/prometheus_metrics.md
index d7779232d3e72..6a4a85f14ffa7 100644
--- a/doc/development/prometheus_metrics.md
+++ b/doc/development/prometheus_metrics.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
---
diff --git a/doc/development/runner_fleet_dashboard.md b/doc/development/runner_fleet_dashboard.md
index 9629dc973c565..737ee751b7f3c 100644
--- a/doc/development/runner_fleet_dashboard.md
+++ b/doc/development/runner_fleet_dashboard.md
@@ -59,6 +59,7 @@ To setup ClickHouse as the GitLab data storage:
1. [Run ClickHouse Cluster and configure database](#run-and-configure-clickhouse).
1. [Configure GitLab connection to Clickhouse](#configure-the-gitlab-connection-to-clickhouse).
+1. [Run ClickHouse migrations](#run-clickhouse-migrations).
1. [Enable the feature flags](#enable-feature-flags).
### Run and configure ClickHouse
diff --git a/doc/operations/incident_management/alerts.md b/doc/operations/incident_management/alerts.md
index f92739580bdfd..2f2cb91c51150 100644
--- a/doc/operations/incident_management/alerts.md
+++ b/doc/operations/incident_management/alerts.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/escalation_policies.md b/doc/operations/incident_management/escalation_policies.md
index 3e1585c930dc1..10dce56024e79 100644
--- a/doc/operations/incident_management/escalation_policies.md
+++ b/doc/operations/incident_management/escalation_policies.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/incident_timeline_events.md b/doc/operations/incident_management/incident_timeline_events.md
index f5322e0a7ccd3..96827183086b0 100644
--- a/doc/operations/incident_management/incident_timeline_events.md
+++ b/doc/operations/incident_management/incident_timeline_events.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/incidents.md b/doc/operations/incident_management/incidents.md
index 43df69a3b39fa..29a7be1e6c9f3 100644
--- a/doc/operations/incident_management/incidents.md
+++ b/doc/operations/incident_management/incidents.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/index.md b/doc/operations/incident_management/index.md
index 1cae066ce32af..377073a9e8a6f 100644
--- a/doc/operations/incident_management/index.md
+++ b/doc/operations/incident_management/index.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/integrations.md b/doc/operations/incident_management/integrations.md
index 8691d78c4c983..a2006aedad0d9 100644
--- a/doc/operations/incident_management/integrations.md
+++ b/doc/operations/incident_management/integrations.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/linked_resources.md b/doc/operations/incident_management/linked_resources.md
index 57fcb31e3ba7c..0f2fccce00c8d 100644
--- a/doc/operations/incident_management/linked_resources.md
+++ b/doc/operations/incident_management/linked_resources.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/manage_incidents.md b/doc/operations/incident_management/manage_incidents.md
index 1b48de9e478d7..9ab9a313a5155 100644
--- a/doc/operations/incident_management/manage_incidents.md
+++ b/doc/operations/incident_management/manage_incidents.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/oncall_schedules.md b/doc/operations/incident_management/oncall_schedules.md
index e885cbd458955..314a801c58013 100644
--- a/doc/operations/incident_management/oncall_schedules.md
+++ b/doc/operations/incident_management/oncall_schedules.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/paging.md b/doc/operations/incident_management/paging.md
index 845c17d974a2f..7247a490d4f36 100644
--- a/doc/operations/incident_management/paging.md
+++ b/doc/operations/incident_management/paging.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/slack.md b/doc/operations/incident_management/slack.md
index 100aaa0d9bcc1..41e731267ca3d 100644
--- a/doc/operations/incident_management/slack.md
+++ b/doc/operations/incident_management/slack.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/incident_management/status_page.md b/doc/operations/incident_management/status_page.md
index 3835ea3078a99..05c6fd9e17e95 100644
--- a/doc/operations/incident_management/status_page.md
+++ b/doc/operations/incident_management/status_page.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/operations/index.md b/doc/operations/index.md
index 766047e1f29ff..a4cd58e69867c 100644
--- a/doc/operations/index.md
+++ b/doc/operations/index.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/user/crm/index.md b/doc/user/crm/index.md
index 46e395af5bc5d..9292c271fd229 100644
--- a/doc/user/crm/index.md
+++ b/doc/user/crm/index.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/user/group/ssh_certificates.md b/doc/user/group/ssh_certificates.md
new file mode 100644
index 0000000000000..84ac79bc7736f
--- /dev/null
+++ b/doc/user/group/ssh_certificates.md
@@ -0,0 +1,83 @@
+---
+stage: Create
+group: Source Code
+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
+---
+
+# Manage group's SSH certificates **(PREMIUM SAAS)**
+
+Manage Git access to the projects by sharing public Certified Authority (`CA`) files in your organization's top-level group.
+
+Git access control options on GitLab SaaS (SSH, HTTPS) rely on credentials (such as access tokens and SSH keys)
+setup in the user profile and are out of control of the organization.
+To temporarily grant Git access to your projects, you can use SSH certificates.
+
+## Add a CA certificate to a top-level group
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421915) in GitLab 16.4 [with a flag](../feature_flags.md) named `ssh_certificates_rest_endpoints`. Disabled by default.
+
+FLAG:
+On GitLab.com, this feature is not available.
+Prerequisites:
+
+- You must have the Owner role for the group.
+- The group must be a top-level group, not a subgroup.
+
+To add a CA certificate to a group:
+
+1. Generate an SSH key pair to be used as a Certified Authority file:
+
+ ```plaintext
+ ssh-keygen -f CA
+ ```
+
+1. Add the public key to the top-level group using [Group SSH certificates API](../../api/group_ssh_certificates.md#create-ssh-certificate)
+ to grant access to the projects of the group and its subgroups.
+
+## Issue CA certificates for users
+
+Prerequisites:
+
+- You must have the Owner role for the group.
+- The user certificates can only be used to access the projects within the top-level group and its subgroups.
+- A user's username or primary email (`user` or `user@example.com`) must be specified to associate a
+ GitLab user with the user certificate.
+- The user must be an [Enterprise User](../enterprise_user/index.md).
+
+To issue user certificates, use the private key from the pair you [created earlier](#add-a-ca-certificate-to-a-top-level-group):
+
+```shell
+ssh-keygen -s CA -I user@example.com -V +1d user-key.pub
+```
+
+The (`user-key.pub`) key is the public key from an SSH key pair that is used by a user for SSH authentication.
+The SSH key pair is either generated by a user or provisioned by the group owner infrastructure along with the SSH certificate.
+
+The expiration date (`+1d`) identifies how long the SSH certificate can be used to access the group projects.
+
+The user certificates can only be used to access the projects within the top-level group.
+
+## Enforce SSH certificates
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/421915) in GitLab 16.7 [with a flag](../feature_flags.md) named `enforce_ssh_certificates_via_settings`. Disabled by default.
+
+FLAG:
+On GitLab.com, this feature is not available.
+
+You can enforce usage of SSH certificates and forbid users from authenticating using SSH
+keys and access tokens.
+
+When SSH certificates are enforced, only individual user accounts are affected.
+It does not apply to service accounts, deploy keys, and other types of internal accounts.
+
+Prerequisites:
+
+- You must have the Owner role for the group.
+
+To enforce using SSH certificates:
+
+1. On the left sidebar, select **Search or go to** and find your group.
+1. Select **Settings > General**.
+1. Expand the **Permissions and group features** section.
+1. Select the **Enforce SSH Certificates** checkbox.
+1. Select **Save changes**.
diff --git a/doc/user/project/service_desk/configure.md b/doc/user/project/service_desk/configure.md
index 8a9e28a6d0d04..a3d2528f69164 100644
--- a/doc/user/project/service_desk/configure.md
+++ b/doc/user/project/service_desk/configure.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/user/project/service_desk/index.md b/doc/user/project/service_desk/index.md
index e87a0bb29439e..9539fe5eb88ca 100644
--- a/doc/user/project/service_desk/index.md
+++ b/doc/user/project/service_desk/index.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/doc/user/project/service_desk/using_service_desk.md b/doc/user/project/service_desk/using_service_desk.md
index 5f3c725b83b6b..8c7677a0cd443 100644
--- a/doc/user/project/service_desk/using_service_desk.md
+++ b/doc/user/project/service_desk/using_service_desk.md
@@ -1,5 +1,5 @@
---
-stage: Monitor
+stage: Service Management
group: Respond
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
---
diff --git a/lib/gitlab/ci/config/entry/retry.rb b/lib/gitlab/ci/config/entry/retry.rb
index e9cbcb31e21c2..f225ed4caf42a 100644
--- a/lib/gitlab/ci/config/entry/retry.rb
+++ b/lib/gitlab/ci/config/entry/retry.rb
@@ -35,8 +35,8 @@ class FullRetry < ::Gitlab::Config::Entry::Node
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[max when].freeze
- attributes :max, :when
+ ALLOWED_KEYS = %i[max when exit_codes].freeze
+ attributes ALLOWED_KEYS
validations do
validates :config, allowed_keys: ALLOWED_KEYS
@@ -53,6 +53,7 @@ class FullRetry < ::Gitlab::Config::Entry::Node
validates :when,
inclusion: { in: FullRetry.possible_retry_when_values },
if: -> (config) { config.when.is_a?(String) }
+ validates :exit_codes, array_of_integers_or_integer: true
end
end
@@ -62,9 +63,14 @@ def self.possible_retry_when_values
def value
super.tap do |config|
- # make sure that `when` is an array, because we allow it to
- # be passed as a String in config for simplicity
+ # make sure that `when` and `exit_codes` are arrays, because we allow them to
+ # be passed as a String/Integer in config for simplicity
config[:when] = Array.wrap(config[:when]) if config[:when]
+ if config[:exit_codes] && Feature.enabled?(:ci_retry_on_exit_codes, Feature.current_request)
+ config[:exit_codes] = Array.wrap(config[:exit_codes])
+ else
+ config.delete(:exit_codes)
+ end
end
end
diff --git a/lib/gitlab/circuit_breaker.rb b/lib/gitlab/circuit_breaker.rb
new file mode 100644
index 0000000000000..2b3a6187f273b
--- /dev/null
+++ b/lib/gitlab/circuit_breaker.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# A configurable circuit breaker to protect the application from external service failures.
+# The circuit measures the amount of failures and if the threshold is exceeded, stops sending requests.
+module Gitlab
+ module CircuitBreaker
+ InternalServerError = Class.new(StandardError)
+
+ DEFAULT_ERROR_THRESHOLD = 50
+ DEFAULT_VOLUME_THRESHOLD = 10
+
+ class << self
+ include ::Gitlab::Utils::StrongMemoize
+
+ # @param [String] unique name for the circuit
+ # @param options [Hash] an options hash setting optional values per circuit
+ def run_with_circuit(service_name, options = {}, &block)
+ circuit(service_name, options).run(exception: false, &block)
+ end
+
+ private
+
+ def circuit(service_name, options)
+ strong_memoize_with(:circuit, service_name, options) do
+ circuit_options = {
+ exceptions: [InternalServerError],
+ error_threshold: DEFAULT_ERROR_THRESHOLD,
+ volume_threshold: DEFAULT_VOLUME_THRESHOLD
+ }.merge(options)
+
+ Circuitbox.circuit(service_name, circuit_options)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/circuit_breaker/notifier.rb b/lib/gitlab/circuit_breaker/notifier.rb
new file mode 100644
index 0000000000000..b555158ee48b6
--- /dev/null
+++ b/lib/gitlab/circuit_breaker/notifier.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CircuitBreaker
+ class Notifier
+ CircuitBreakerError = Class.new(RuntimeError)
+
+ def notify(service_name, event)
+ return unless event == 'failure'
+
+ exception = CircuitBreakerError.new("Service #{service_name}: #{event}")
+ exception.set_backtrace(Gitlab::BacktraceCleaner.clean_backtrace(caller))
+
+ Gitlab::ErrorTracking.track_exception(exception)
+ end
+
+ def notify_warning(_service_name, _message)
+ # no-op
+ end
+
+ def notify_run(_service_name, &_block)
+ # This gets called by Circuitbox::CircuitBreaker#run to actually execute
+ # the block passed.
+ yield
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/circuit_breaker/store.rb b/lib/gitlab/circuit_breaker/store.rb
new file mode 100644
index 0000000000000..0ba4f08d5e17a
--- /dev/null
+++ b/lib/gitlab/circuit_breaker/store.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module CircuitBreaker
+ class Store
+ def key?(key)
+ with { |redis| redis.exists?(key) }
+ end
+
+ def store(key, value, opts = {})
+ with do |redis|
+ redis.set(key, value, ex: opts[:expires])
+ value
+ end
+ end
+
+ def increment(key, amount = 1, opts = {})
+ expires = opts[:expires]
+
+ with do |redis|
+ redis.multi do |multi|
+ multi.incrby(key, amount)
+ multi.expire(key, expires) if expires
+ end
+ end
+ end
+
+ def load(key, _opts = {})
+ with { |redis| redis.get(key) }
+ end
+
+ def values_at(*keys, **_opts)
+ keys.map! { |key| load(key) }
+ end
+
+ def delete(key)
+ with { |redis| redis.del(key) }
+ end
+
+ private
+
+ def with(&block)
+ Gitlab::Redis::RateLimiting.with(&block)
+ rescue ::Redis::BaseConnectionError
+ # Do not raise an error if we cannot connect to Redis. If
+ # Redis::RateLimiting is unavailable it should not take the site down.
+ nil
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/quick_actions/merge_request_actions.rb b/lib/gitlab/quick_actions/merge_request_actions.rb
index 72bec159226f2..fe18bc8e133be 100644
--- a/lib/gitlab/quick_actions/merge_request_actions.rb
+++ b/lib/gitlab/quick_actions/merge_request_actions.rb
@@ -161,10 +161,25 @@ module MergeRequestActions
condition do
quick_action_target.persisted?
end
- command :submit_review do
+ command :submit_review do |state = "reviewed"|
next if params[:review_id]
result = DraftNotes::PublishService.new(quick_action_target, current_user).execute
+
+ if Feature.enabled?(:mr_request_changes, current_user)
+ reviewer_state = state.strip.presence
+
+ if reviewer_state === 'approve'
+ ::MergeRequests::ApprovalService
+ .new(project: quick_action_target.project, current_user: current_user)
+ .execute(quick_action_target)
+ elsif MergeRequestReviewer.states.key?(reviewer_state)
+ ::MergeRequests::UpdateReviewerStateService
+ .new(project: quick_action_target.project, current_user: current_user)
+ .execute(quick_action_target, reviewer_state)
+ end
+ end
+
@execution_message[:submit_review] = if result[:status] == :success
_('Submitted the current review.')
else
diff --git a/lib/product_analytics/event_params.rb b/lib/product_analytics/event_params.rb
deleted file mode 100644
index 6cb3d462384c6..0000000000000
--- a/lib/product_analytics/event_params.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-# frozen_string_literal: true
-
-module ProductAnalytics
- # Converts params from Snowplow tracker to one compatible with
- # GitLab ProductAnalyticsEvent model. The field naming corresponds
- # with snowplow event model. Only project_id is GitLab specific.
- #
- # For information on what each field is you can check next resources:
- # * Snowplow tracker protocol: https://github.com/snowplow/snowplow/wiki/snowplow-tracker-protocol
- # * Canonical event model: https://github.com/snowplow/snowplow/wiki/canonical-event-model
- class EventParams
- def self.parse_event_params(params)
- {
- project_id: params['aid'],
- platform: params['p'],
- collector_tstamp: Time.zone.now,
- event_id: params['eid'],
- v_tracker: params['tv'],
- v_collector: Gitlab::VERSION,
- v_etl: Gitlab::VERSION,
- os_timezone: params['tz'],
- name_tracker: params['tna'],
- br_lang: params['lang'],
- doc_charset: params['cs'],
- br_features_pdf: Gitlab::Utils.to_boolean(params['f_pdf']),
- br_features_flash: Gitlab::Utils.to_boolean(params['f_fla']),
- br_features_java: Gitlab::Utils.to_boolean(params['f_java']),
- br_features_director: Gitlab::Utils.to_boolean(params['f_dir']),
- br_features_quicktime: Gitlab::Utils.to_boolean(params['f_qt']),
- br_features_realplayer: Gitlab::Utils.to_boolean(params['f_realp']),
- br_features_windowsmedia: Gitlab::Utils.to_boolean(params['f_wma']),
- br_features_gears: Gitlab::Utils.to_boolean(params['f_gears']),
- br_features_silverlight: Gitlab::Utils.to_boolean(params['f_ag']),
- br_colordepth: params['cd'],
- br_cookies: Gitlab::Utils.to_boolean(params['cookie']),
- dvce_created_tstamp: params['dtm'],
- br_viewheight: params['vp'],
- domain_sessionidx: params['vid'],
- domain_sessionid: params['sid'],
- domain_userid: params['duid'],
- user_fingerprint: params['fp'],
- page_referrer: params['refr'],
- page_url: params['url'],
- se_category: params['se_ca'],
- se_action: params['se_ac'],
- se_label: params['se_la'],
- se_property: params['se_pr'],
- se_value: params['se_va']
- }
- end
-
- def self.has_required_params?(params)
- params['aid'].present? && params['eid'].present?
- end
- end
-end
diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb
index 3ea29ff63da24..feb57a1cbf6a0 100644
--- a/qa/qa/page/component/clone_panel.rb
+++ b/qa/qa/page/component/clone_panel.rb
@@ -9,7 +9,7 @@ module ClonePanel
def self.included(base)
super
- base.view 'app/views/projects/buttons/_clone.html.haml' do
+ base.view 'app/views/projects/buttons/_code.html.haml' do
element :clone_dropdown
element :clone_dropdown_content
element :ssh_clone_url_content
diff --git a/rubocop/rubocop-code_reuse.yml b/rubocop/rubocop-code_reuse.yml
index 6f9d7902dc681..cf81aa2db5e6d 100644
--- a/rubocop/rubocop-code_reuse.yml
+++ b/rubocop/rubocop-code_reuse.yml
@@ -26,6 +26,7 @@ CodeReuse/ActiveRecord:
- lib/banzai/**/*.rb
- lib/click_house/migration_support/**/*.rb
- lib/gitlab/background_migration/**/*.rb
+ - lib/gitlab/circuit_breaker/store.rb
- lib/gitlab/cycle_analytics/**/*.rb
- lib/gitlab/counters/**/*.rb
- lib/gitlab/database/**/*.rb
diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb
index 6f2cd4bf59693..c4b318a5aeef4 100644
--- a/spec/factories/environments.rb
+++ b/spec/factories/environments.rb
@@ -35,6 +35,14 @@
name { 'development' }
end
+ trait :with_folders do |environment|
+ sequence(:name) { |n| "#{folder}/environment#{n}" }
+
+ transient do
+ folder { 'folder' }
+ end
+ end
+
trait :with_review_app do |environment|
sequence(:name) { |n| "review/#{n}" }
diff --git a/spec/factories/product_analytics_event.rb b/spec/factories/product_analytics_event.rb
deleted file mode 100644
index 168b255f6ca88..0000000000000
--- a/spec/factories/product_analytics_event.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-FactoryBot.define do
- factory :product_analytics_event do
- project
- platform { 'web' }
- collector_tstamp { DateTime.now }
- dvce_created_tstamp { DateTime.now }
- event_id { SecureRandom.uuid }
- name_tracker { 'sp' }
- v_tracker { 'js-2.14.0' }
- v_collector { 'GitLab 13.1.0-pre' }
- v_etl { 'GitLab 13.1.0-pre' }
- domain_userid { SecureRandom.uuid }
- domain_sessionidx { 4 }
- page_url { 'http://localhost:3333/products/123' }
- br_lang { 'en-US' }
- br_cookies { true }
- br_colordepth { '24' }
- os_timezone { 'America/Los_Angeles' }
- doc_charset { 'UTF-8' }
- domain_sessionid { SecureRandom.uuid }
- end
-end
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 5379dabc71361..e9b55ab2900d7 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -151,7 +151,7 @@
it 'shows that the last pipeline passed' do
visit dashboard_projects_path
- page.within('[data-testid="project_controls"]') do
+ within_testid('project_controls') do
expect(page).to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).to have_css("[data-testid='ci-icon']")
expect(page).to have_css('[data-testid="status_success_borderless-icon"]')
@@ -163,7 +163,7 @@
it 'does not show the pipeline status' do
visit dashboard_projects_path
- page.within('[data-testid="project_controls"]') do
+ within_testid('project_controls') do
expect(page).not_to have_xpath("//a[@href='#{pipelines_project_commit_path(project, project.commit, ref: pipeline.ref)}']")
expect(page).not_to have_css("[data-testid='ci-icon']")
expect(page).not_to have_css('[data-testid="status_success_borderless-icon"]')
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index 59ce873905a4e..21dfa1cbc0bca 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -95,7 +95,7 @@
shared_examples 'deleting the todo' do
before do
within first('.todo') do
- find('[data-testid="check-icon"]').click
+ find_by_testid('check-icon').click
end
end
@@ -121,9 +121,9 @@
shared_examples 'deleting and restoring the todo' do
before do
within first('.todo') do
- find('[data-testid="check-icon"]').click
+ find_by_testid('check-icon').click
wait_for_requests
- find('[data-testid="redo-icon"]').click
+ find_by_testid('redo-icon').click
end
end
@@ -301,7 +301,7 @@
describe 'restoring the todo' do
before do
within first('.todo') do
- find('[data-testid="todo-add-icon"]').click
+ find_by_testid('todo-add-icon').click
end
end
@@ -407,7 +407,7 @@
context 'User has deleted a todo' do
before do
within first('.todo') do
- find('[data-testid="check-icon"]').click
+ find_by_testid('check-icon').click
end
end
diff --git a/spec/features/environments/environments_folder_spec.rb b/spec/features/environments/environments_folder_spec.rb
new file mode 100644
index 0000000000000..ed5e3e9833816
--- /dev/null
+++ b/spec/features/environments/environments_folder_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Environments Folder page', :js, feature_category: :environment_management do
+ let(:folder_name) { 'folder' }
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let!(:envs) { create_list(:environment, 4, :with_folders, project: project, folder: folder_name) }
+
+ before_all do
+ project.add_role(user, :developer)
+ end
+
+ before do
+ create(:environment, :production, project: project)
+ end
+
+ describe 'new folders page' do
+ before do
+ sign_in(user)
+ visit folder_project_environments_path(project, folder_name)
+ wait_for_requests
+ end
+
+ it 'renders the header with a folder name' do
+ expect(page).to have_content("Environments / #{folder_name}")
+ end
+ end
+
+ describe 'legacy folders page' do
+ before do
+ stub_feature_flags(environments_folder_new_look: false)
+ sign_in(user)
+ visit folder_project_environments_path(project, folder_name)
+ wait_for_requests
+ end
+
+ it 'user opens folder view' do
+ expect(page).to have_content("Environments / #{folder_name}")
+ expect(page).not_to have_content('production')
+ envs.each { |env| expect(page).to have_content(env.name.split('/').last) }
+ end
+ end
+end
diff --git a/spec/features/groups/board_sidebar_spec.rb b/spec/features/groups/board_sidebar_spec.rb
index 3fe520ea2ea13..d03fd5323271b 100644
--- a/spec/features/groups/board_sidebar_spec.rb
+++ b/spec/features/groups/board_sidebar_spec.rb
@@ -34,7 +34,7 @@
wait_for_requests
- page.within('[data-testid="dropdown-content"]') do
+ within_testid('dropdown-content') do
expect(page).to have_content(project_1_label.title)
expect(page).to have_content(group_label.title)
expect(page).not_to have_content(project_2_label.title)
diff --git a/spec/features/groups/board_spec.rb b/spec/features/groups/board_spec.rb
index e6dc6055e27a2..1a9a53a7421ce 100644
--- a/spec/features/groups/board_spec.rb
+++ b/spec/features/groups/board_spec.rb
@@ -32,7 +32,7 @@
fill_in 'issue_title', with: issue_title
- page.within("[data-testid='project-select-dropdown']") do
+ within_testid('project-select-dropdown') do
find('button.gl-new-dropdown-toggle').click
find('.gl-new-dropdown-item').click
diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb
index d876a5804bd6e..ca0687134b643 100644
--- a/spec/features/groups/clusters/user_spec.rb
+++ b/spec/features/groups/clusters/user_spec.rb
@@ -112,9 +112,9 @@
context 'when user destroys the cluster' do
before do
click_link 'Advanced Settings'
- find('[data-testid="remove-integration-button"]').click
+ find_by_testid('remove-integration-button').click
fill_in 'confirm_cluster_name_input', with: cluster.name
- find('[data-testid="remove-integration-modal-button"]').click
+ find_by_testid('remove-integration-modal-button').click
end
it 'user sees creation form with the successful message' do
diff --git a/spec/features/groups/dependency_proxy_spec.rb b/spec/features/groups/dependency_proxy_spec.rb
index 12c480a46b006..136c1ff0335f1 100644
--- a/spec/features/groups/dependency_proxy_spec.rb
+++ b/spec/features/groups/dependency_proxy_spec.rb
@@ -62,7 +62,7 @@
visit settings_path
wait_for_requests
- proxy_toggle = find('[data-testid="dependency-proxy-setting-toggle"]')
+ proxy_toggle = find_by_testid('dependency-proxy-setting-toggle')
proxy_toggle_button = proxy_toggle.find('button')
expect(proxy_toggle).to have_css("button.is-checked")
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 0437e5df6e9e0..a7710ea04ab3b 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -141,8 +141,8 @@
end
describe 'transfer group', :js do
- let(:namespace_select) { page.find('[data-testid="transfer-group-namespace-select"]') }
- let(:confirm_modal) { page.find('[data-testid="confirm-danger-modal"]') }
+ let(:namespace_select) { find_by_testid('transfer-group-namespace-select') }
+ let(:confirm_modal) { find_by_testid('confirm-danger-modal') }
shared_examples 'can transfer the group' do
before do
@@ -154,7 +154,7 @@
visit edit_group_path(selected_group)
- page.within('[data-testid="transfer-locations-dropdown"]') do
+ within_testid('transfer-locations-dropdown') do
click_button _('Select parent group')
fill_in _('Search'), with: target_group&.name || ''
wait_for_requests
@@ -170,7 +170,7 @@
click_button 'Confirm'
end
- within('[data-testid="breadcrumb-links"]') do
+ within_testid('breadcrumb-links') do
expect(page).to have_content(target_group.name) if target_group
expect(page).to have_content(selected_group.name)
end
diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb
index d864852e0d46c..27ceec05fe3ab 100644
--- a/spec/features/groups/members/leave_group_spec.rb
+++ b/spec/features/groups/members/leave_group_spec.rb
@@ -79,7 +79,7 @@
visit group_path(group, leave: 1)
- expect(find('[data-testid="alert-danger"]')).to have_content 'You do not have permission to leave this group'
+ expect(find_by_testid('alert-danger')).to have_content 'You do not have permission to leave this group'
end
def left_group_message(group)
diff --git a/spec/features/groups/members/manage_groups_spec.rb b/spec/features/groups/members/manage_groups_spec.rb
index 87de0e2e46b79..9531ebd3c35fd 100644
--- a/spec/features/groups/members/manage_groups_spec.rb
+++ b/spec/features/groups/members/manage_groups_spec.rb
@@ -106,7 +106,7 @@
page.within first_row do
expect(page).to have_field('Expiration date', with: expiration_date)
- find('[data-testid="clear-button"]').click
+ find_by_testid('clear-button').click
wait_for_requests
diff --git a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
index c2eedfb40632f..38eb226690c62 100644
--- a/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
+++ b/spec/features/groups/members/master_adds_member_with_expiration_date_spec.rb
@@ -48,7 +48,7 @@
page.within second_row do
expect(page).to have_field('Expiration date', with: expiration_date)
- find('[data-testid="clear-button"]').click
+ find_by_testid('clear-button').click
wait_for_requests
diff --git a/spec/features/groups/members/search_members_spec.rb b/spec/features/groups/members/search_members_spec.rb
index ed2e0cd7b09f9..5f4ff8023c68a 100644
--- a/spec/features/groups/members/search_members_spec.rb
+++ b/spec/features/groups/members/search_members_spec.rb
@@ -21,7 +21,7 @@
end
it 'renders member users' do
- page.within '[data-testid="members-filtered-search-bar"]' do
+ within_testid('members-filtered-search-bar') do
find_field('Filter members').click
find('input').native.send_keys(member.name)
click_button 'Search'
diff --git a/spec/features/groups/members/sort_members_spec.rb b/spec/features/groups/members/sort_members_spec.rb
index ea6f3ae1966fd..1cc9862ff3b91 100644
--- a/spec/features/groups/members/sort_members_spec.rb
+++ b/spec/features/groups/members/sort_members_spec.rb
@@ -17,7 +17,7 @@
end
def expect_sort_by(text, sort_direction)
- within('[data-testid="members-sort-dropdown"]') do
+ within_testid('members-sort-dropdown') do
expect(page).to have_css('button[aria-haspopup="menu"]', text: text)
expect(page).to have_button("Sort direction: #{sort_direction == :asc ? 'Ascending' : 'Descending'}")
end
diff --git a/spec/features/groups/members/tabs_spec.rb b/spec/features/groups/members/tabs_spec.rb
index cc97b36731372..b96aa9293f221 100644
--- a/spec/features/groups/members/tabs_spec.rb
+++ b/spec/features/groups/members/tabs_spec.rb
@@ -62,7 +62,7 @@
click_link 'Invited'
- page.within '[data-testid="members-filtered-search-bar"]' do
+ within_testid('members-filtered-search-bar') do
find_field('Search invited').click
find('input').native.send_keys('email')
click_button 'Search'
@@ -75,7 +75,7 @@
before do
click_link 'Members'
- page.within '[data-testid="members-filtered-search-bar"]' do
+ within_testid 'members-filtered-search-bar' do
find_field('Filter members').click
find('input').native.send_keys('test')
click_button 'Search'
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 0a830e6715c95..84a6bc96df0a6 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -81,7 +81,7 @@
it 'shows projects only with merge requests feature enabled', :js do
click_button 'Select project to create merge request'
- page.within('[data-testid="new-resource-dropdown"]') do
+ within_testid('new-resource-dropdown') do
expect(page).to have_content(project.name_with_namespace)
expect(page).not_to have_content(project_with_merge_requests_disabled.name_with_namespace)
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 0a54f5923f2c9..c7e0fdb3fc8d1 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -461,22 +461,6 @@ def upcoming_deployment_content_selector
end
end
- describe 'environments folders view' do
- before do
- create(:environment, project: project, name: 'staging.review/review-1', state: :available)
- create(:environment, project: project, name: 'staging.review/review-2', state: :available)
- end
-
- it 'user opens folder view' do
- visit folder_project_environments_path(project, 'staging.review')
- wait_for_requests
-
- expect(page).to have_content('Environments / staging.review')
- expect(page).to have_content('review-1')
- expect(page).to have_content('review-2')
- end
- end
-
def have_terminal_button
have_link(_('Terminal'), href: terminal_project_environment_path(project, environment))
end
diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb
index 61225b457605e..88c16ab4c9f40 100644
--- a/spec/features/projects/user_sees_sidebar_spec.rb
+++ b/spec/features/projects/user_sees_sidebar_spec.rb
@@ -207,7 +207,7 @@
visit project_path(project)
within('.project-repo-buttons') do
- expect(page).not_to have_selector '.project-clone-holder'
+ expect(page).not_to have_selector '.project-code-holder'
end
end
diff --git a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
index 294b5302ec0c0..e9d2e68c1a347 100644
--- a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
+++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
@@ -1,6 +1,7 @@
import { GlBanner, GlButton } from '@gitlab/ui';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '~/ci/catalog/constants';
@@ -16,6 +17,7 @@ describe('CatalogHeader', () => {
};
const findBanner = () => wrapper.findComponent(GlBanner);
+ const findBetaBadge = () => wrapper.findComponent(BetaBadge);
const findFeedbackButton = () => findBanner().findComponent(GlButton);
const findTitle = () => wrapper.find('h1');
const findDescription = () => wrapper.findByTestId('page-description');
@@ -33,6 +35,16 @@ describe('CatalogHeader', () => {
});
};
+ describe('Default view', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a Beta Badge', () => {
+ expect(findBetaBadge().exists()).toBe(true);
+ });
+ });
+
describe('title and description', () => {
describe('when there are no values provided', () => {
beforeEach(() => {
@@ -46,6 +58,7 @@ describe('CatalogHeader', () => {
);
});
});
+
describe('when custom values are provided', () => {
beforeEach(() => {
createComponent({ provide: customProvide });
@@ -57,6 +70,7 @@ describe('CatalogHeader', () => {
});
});
});
+
describe('Feedback banner', () => {
describe('when user has never dismissed', () => {
beforeEach(() => {
diff --git a/spec/frontend/environments/folder/environments_folder_app_spec.js b/spec/frontend/environments/folder/environments_folder_app_spec.js
new file mode 100644
index 0000000000000..6ed279517bd21
--- /dev/null
+++ b/spec/frontend/environments/folder/environments_folder_app_spec.js
@@ -0,0 +1,23 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EnvironmentsFolderAppComponent from '~/environments/folder/environments_folder_app.vue';
+
+describe('EnvironmentsFolderAppComponent', () => {
+ let wrapper;
+ const mockFolderName = 'folders';
+
+ const createWrapper = () => {
+ wrapper = shallowMountExtended(EnvironmentsFolderAppComponent, {
+ propsData: {
+ folderName: mockFolderName,
+ },
+ });
+ };
+
+ const findHeader = () => wrapper.findByTestId('folder-name');
+
+ it('should render a header with the folder name', () => {
+ createWrapper();
+
+ expect(findHeader().text()).toMatchInterpolatedText(`Environments / ${mockFolderName}`);
+ });
+});
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index da465552db3a3..6cea75036bc0f 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -45,6 +45,16 @@ describe('GfmAutoComplete', () => {
let sorterValue;
let filterValue;
+ const triggerDropdown = ($textarea, text) => {
+ $textarea
+ .trigger('focus')
+ .val($textarea.val() + text)
+ .caret('pos', -1);
+ $textarea.trigger('keyup');
+
+ jest.runOnlyPendingTimers();
+ };
+
describe('DefaultOptions.filter', () => {
let items;
@@ -786,13 +796,6 @@ describe('GfmAutoComplete', () => {
resetHTMLFixture();
});
- const triggerDropdown = (text) => {
- $textarea.trigger('focus').val(text).caret('pos', -1);
- $textarea.trigger('keyup');
-
- jest.runOnlyPendingTimers();
- };
-
const getDropdownItems = () => {
const dropdown = document.getElementById('at-view-labels');
const items = dropdown.getElementsByTagName('li');
@@ -800,7 +803,7 @@ describe('GfmAutoComplete', () => {
};
const expectLabels = ({ input, output }) => {
- triggerDropdown(input);
+ triggerDropdown($textarea, input);
expect(getDropdownItems()).toEqual(output.map((label) => label.title));
};
@@ -860,6 +863,50 @@ describe('GfmAutoComplete', () => {
});
});
+ describe('submit_review', () => {
+ let autocomplete;
+ let $textarea;
+
+ const getDropdownItems = () => {
+ const dropdown = document.getElementById('at-view-submit_review');
+
+ return dropdown.getElementsByTagName('li');
+ };
+
+ beforeEach(() => {
+ jest
+ .spyOn(AjaxCache, 'retrieve')
+ .mockReturnValue(Promise.resolve([{ name: 'submit_review' }]));
+
+ window.gon = { features: { mrRequestChanges: true } };
+
+ setHTMLFixture('');
+ autocomplete = new GfmAutoComplete({
+ commands: `${TEST_HOST}/autocomplete_sources/commands`,
+ });
+ $textarea = $('textarea');
+ autocomplete.setup($textarea, {});
+ });
+
+ afterEach(() => {
+ autocomplete.destroy();
+ resetHTMLFixture();
+ });
+
+ it('renders submit review options', async () => {
+ triggerDropdown($textarea, '/');
+
+ await waitForPromises();
+
+ triggerDropdown($textarea, 'submit_review ');
+
+ expect(getDropdownItems()).toHaveLength(3);
+ expect(getDropdownItems()[0].textContent).toContain('Comment');
+ expect(getDropdownItems()[1].textContent).toContain('Approve');
+ expect(getDropdownItems()[2].textContent).toContain('Request changes');
+ });
+ });
+
describe('emoji', () => {
const mockItem = {
'atwho-at': ':',
@@ -951,13 +998,6 @@ describe('GfmAutoComplete', () => {
resetHTMLFixture();
});
- const triggerDropdown = (text) => {
- $textarea.trigger('focus').val(text).caret('pos', -1);
- $textarea.trigger('keyup');
-
- jest.runOnlyPendingTimers();
- };
-
const getDropdownItems = () => {
const dropdown = document.getElementById('at-view-contacts');
const items = dropdown.getElementsByTagName('li');
@@ -965,7 +1005,7 @@ describe('GfmAutoComplete', () => {
};
const expectContacts = ({ input, output }) => {
- triggerDropdown(input);
+ triggerDropdown($textarea, input);
expect(getDropdownItems()).toEqual(
output.map((contact) => `${contact.first_name} ${contact.last_name} ${contact.email}`),
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 9413927ac260f..f607d40253f0b 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -43,11 +43,6 @@ describe('GlobalSearchSidebar', () => {
wrapper = shallowMount(GlobalSearchSidebar, {
store,
- provide: {
- glFeatures: {
- searchProjectWikisHideArchivedProjects: true,
- },
- },
});
};
diff --git a/spec/initializers/circuitbox_spec.rb b/spec/initializers/circuitbox_spec.rb
index 64e384e4d2240..d0a6d6c9f9152 100644
--- a/spec/initializers/circuitbox_spec.rb
+++ b/spec/initializers/circuitbox_spec.rb
@@ -3,12 +3,7 @@
require 'spec_helper'
RSpec.describe 'circuitbox', feature_category: :shared do
- it 'does not configure Circuitbox', unless: Gitlab.ee? do
- expect(Circuitbox.default_circuit_store).to be_a(Circuitbox::MemoryStore)
- expect(Circuitbox.default_notifier).to be_a(Circuitbox::Notifier::ActiveSupport)
- end
-
- it 'configures Circuitbox', if: Gitlab.ee? do
+ it 'configures Circuitbox' do
expect(Circuitbox.default_circuit_store).to be_a(Gitlab::CircuitBreaker::Store)
expect(Circuitbox.default_notifier).to be_a(Gitlab::CircuitBreaker::Notifier)
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 24d3cac66169c..073d8feaadd32 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -789,7 +789,7 @@
hooks: { pre_get_sources_script: 'echo hello' } }
end
- it 'returns correct value' do
+ it 'returns correct values' do
expect(entry.value).to eq(
name: :rspec,
before_script: %w[ls pwd],
@@ -806,6 +806,93 @@
)
end
end
+
+ context 'with retry present in the config' do
+ let(:config) do
+ {
+ script: 'rspec',
+ retry: { max: 1, when: "always" }
+ }
+ end
+
+ it 'returns correct values' do
+ expect(entry.value)
+ .to eq(name: :rspec,
+ script: %w[rspec],
+ stage: 'test',
+ ignore: false,
+ retry: { max: 1, when: %w[always] },
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
+ end
+
+ context 'when ci_retry_on_exit_codes feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_retry_on_exit_codes: false)
+ end
+
+ it 'returns correct values' do
+ expect(entry.value)
+ .to eq(name: :rspec,
+ script: %w[rspec],
+ stage: 'test',
+ ignore: false,
+ retry: { max: 1, when: %w[always] },
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
+ end
+ end
+
+ context 'with exit_codes present' do
+ let(:config) do
+ {
+ script: 'rspec',
+ retry: { max: 1, when: "always", exit_codes: 255 }
+ }
+ end
+
+ it 'returns correct values' do
+ expect(entry.value)
+ .to eq(name: :rspec,
+ script: %w[rspec],
+ stage: 'test',
+ ignore: false,
+ retry: { max: 1, when: %w[always], exit_codes: [255] },
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
+ end
+
+ context 'when ci_retry_on_exit_codes feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_retry_on_exit_codes: false)
+ end
+
+ it 'returns correct values' do
+ expect(entry.value)
+ .to eq(name: :rspec,
+ script: %w[rspec],
+ stage: 'test',
+ ignore: false,
+ # Shouldn't include exit_codes
+ retry: { max: 1, when: %w[always] },
+ only: { refs: %w[branches tags] },
+ job_variables: {},
+ root_variables_inheritance: true,
+ scheduling_type: :stage
+ )
+ end
+ end
+ end
+ end
end
context 'when job is using tags' do
diff --git a/spec/lib/gitlab/ci/config/entry/retry_spec.rb b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
index 84ef5344a8b37..e01b50c5fbd1d 100644
--- a/spec/lib/gitlab/ci/config/entry/retry_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/retry_spec.rb
@@ -11,8 +11,9 @@
end
shared_context 'when retry value is a hash', :hash do
- let(:config) { { max: max, when: public_send(:when) }.compact }
+ let(:config) { { max: max, when: public_send(:when), exit_codes: public_send(:exit_codes) }.compact }
let(:when) {}
+ let(:exit_codes) {}
let(:max) {}
end
@@ -43,6 +44,44 @@
expect(value).to eq(when: %w[unknown_failure runner_system_failure])
end
end
+
+ context 'and `exit_codes` is an integer' do
+ let(:exit_codes) { 255 }
+
+ it 'returns an array of exit_codes' do
+ expect(value).to eq(exit_codes: [255])
+ end
+ end
+
+ context 'and `exit_codes` is an array' do
+ let(:exit_codes) { [255, 142] }
+
+ it 'returns an array of exit_codes' do
+ expect(value).to eq(exit_codes: [255, 142])
+ end
+ end
+ end
+
+ context 'when ci_retry_on_exit_codes feature flag is disabled', :hash do
+ before do
+ stub_feature_flags(ci_retry_on_exit_codes: false)
+ end
+
+ context 'when `exit_codes` is an integer' do
+ let(:exit_codes) { 255 }
+
+ it 'deletes the attribute exit_codes' do
+ expect(value).to eq({})
+ end
+ end
+
+ context 'when `exit_codes` is an array' do
+ let(:exit_codes) { [255, 137] }
+
+ it 'deletes the attribute exit_codes' do
+ expect(value).to eq({})
+ end
+ end
end
end
@@ -65,6 +104,22 @@
end
end
+ context 'with numeric exit_codes' do
+ let(:exit_codes) { 255 }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'with hash values exit_codes' do
+ let(:exit_codes) { [255, 142] }
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
context 'with string when' do
let(:when) { 'unknown_failure' }
@@ -202,7 +257,7 @@
end
end
- context 'iwth max too high' do
+ context 'with max too high' do
let(:max) { 10 }
it 'returns error about value too high' do
@@ -211,6 +266,33 @@
end
end
+ context 'with exit_codes in wrong format' do
+ let(:exit_codes) { true }
+
+ it 'raises an error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry exit codes should be an array of integers or an integer'
+ end
+ end
+
+ context 'with exit_codes in wrong array format' do
+ let(:exit_codes) { ['string 1', 'string 2'] }
+
+ it 'raises an error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry exit codes should be an array of integers or an integer'
+ end
+ end
+
+ context 'with exit_codes in wrong mixed array format' do
+ let(:exit_codes) { [255, '155'] }
+
+ it 'raises an error' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include 'retry exit codes should be an array of integers or an integer'
+ end
+ end
+
context 'with when in wrong format' do
let(:when) { true }
diff --git a/spec/lib/gitlab/circuit_breaker/notifier_spec.rb b/spec/lib/gitlab/circuit_breaker/notifier_spec.rb
new file mode 100644
index 0000000000000..1640ebb99f967
--- /dev/null
+++ b/spec/lib/gitlab/circuit_breaker/notifier_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::CircuitBreaker::Notifier, feature_category: :shared do
+ subject(:instance) { described_class.new }
+
+ describe '#notify' do
+ context 'when event is failure' do
+ it 'sends an exception to Gitlab::ErrorTracking' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+
+ instance.notify('test_service', 'failure')
+ end
+ end
+
+ context 'when event is not failure' do
+ it 'does not send an exception to Gitlab::ErrorTracking' do
+ expect(Gitlab::ErrorTracking).not_to receive(:track_exception)
+
+ instance.notify('test_service', 'test_event')
+ end
+ end
+ end
+
+ describe '#notify_warning' do
+ it do
+ expect { instance.notify_warning('test_service', 'test_message') }.not_to raise_error
+ end
+ end
+
+ describe '#notify_run' do
+ it do
+ expect { instance.notify_run('test_service') { puts 'test block' } }.not_to raise_error
+ end
+ end
+end
diff --git a/spec/lib/gitlab/circuit_breaker/store_spec.rb b/spec/lib/gitlab/circuit_breaker/store_spec.rb
new file mode 100644
index 0000000000000..1b1983d4b529e
--- /dev/null
+++ b/spec/lib/gitlab/circuit_breaker/store_spec.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::CircuitBreaker::Store, :clean_gitlab_redis_rate_limiting, feature_category: :ai_abstraction_layer do
+ let(:key) { 'key-1' }
+ let(:value) { 'value' }
+ let(:circuit_store) { described_class.new }
+
+ shared_examples 'reliable circuit breaker store method' do
+ it 'does not raise an error when Redis::BaseConnectionError is encountered' do
+ allow(Gitlab::Redis::RateLimiting)
+ .to receive(:with)
+ .and_raise(Redis::BaseConnectionError)
+
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ describe '#key?' do
+ subject(:key?) { circuit_store.key?(key) }
+
+ it_behaves_like 'reliable circuit breaker store method'
+
+ context 'when key exists' do
+ before do
+ circuit_store.store(key, value)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when key does not exist' do
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#store' do
+ let(:options) { {} }
+
+ subject(:store) { circuit_store.store(key, value, options) }
+
+ it_behaves_like 'reliable circuit breaker store method'
+
+ it 'stores value for specified key without expiry by default' do
+ expect(store).to eq(value)
+
+ with_redis do |redis|
+ expect(redis.get(key)).to eq(value)
+ expect(redis.ttl(key)).to eq(-1)
+ end
+ end
+
+ context 'when expires option is set' do
+ let(:options) { { expires: 10 } }
+
+ it 'stores value for specified key with expiry' do
+ expect(store).to eq(value)
+
+ with_redis do |redis|
+ expect(redis.get(key)).to eq(value)
+ expect(redis.ttl(key)).to eq(10)
+ end
+ end
+ end
+ end
+
+ describe '#increment' do
+ let(:options) { {} }
+
+ subject(:increment) { circuit_store.increment(key, 1, options) }
+
+ it_behaves_like 'reliable circuit breaker store method'
+
+ context 'when key does not exist' do
+ it 'sets key and increments value' do
+ increment
+
+ with_redis do |redis|
+ expect(redis.get(key).to_i).to eq(1)
+ expect(redis.ttl(key)).to eq(-1)
+ end
+ end
+
+ context 'with expiry' do
+ let(:options) { { expires: 10 } }
+
+ it 'sets key and increments value with expiration' do
+ increment
+
+ with_redis do |redis|
+ expect(redis.get(key).to_i).to eq(1)
+ expect(redis.ttl(key)).to eq(10)
+ end
+ end
+ end
+ end
+
+ context 'when key exists' do
+ before do
+ circuit_store.store(key, 1)
+ end
+
+ it 'increments value' do
+ increment
+
+ with_redis do |redis|
+ expect(redis.get(key).to_i).to eq(2)
+ expect(redis.ttl(key)).to eq(-1)
+ end
+ end
+
+ context 'with expiry' do
+ let(:options) { { expires: 10 } }
+
+ it 'increments value with expiration' do
+ increment
+
+ with_redis do |redis|
+ expect(redis.get(key).to_i).to eq(2)
+ expect(redis.ttl(key)).to eq(10)
+ end
+ end
+ end
+ end
+ end
+
+ describe '#load' do
+ subject(:load) { circuit_store.load(key) }
+
+ it_behaves_like 'reliable circuit breaker store method'
+
+ context 'when key exists' do
+ before do
+ circuit_store.store(key, value)
+ end
+
+ it 'returns the value of the key' do
+ expect(load).to eq(value)
+ end
+ end
+
+ context 'when key does not exist' do
+ it 'returns nil' do
+ expect(load).to be_nil
+ end
+ end
+ end
+
+ describe '#values_at' do
+ let(:other_key) { 'key-2' }
+ let(:other_value) { 'value-2' }
+
+ subject(:values_at) { circuit_store.values_at(key, other_key) }
+
+ it_behaves_like 'reliable circuit breaker store method'
+
+ context 'when keys exist' do
+ before do
+ circuit_store.store(key, value)
+ circuit_store.store(other_key, other_value)
+ end
+
+ it 'returns values of keys' do
+ expect(values_at).to match_array([value, other_value])
+ end
+ end
+
+ context 'when some keys do not exist' do
+ before do
+ circuit_store.store(key, value)
+ end
+
+ it 'returns values of keys with nil for non-existing ones' do
+ expect(values_at).to match_array([value, nil])
+ end
+ end
+ end
+
+ describe '#delete' do
+ subject(:delete) { circuit_store.delete(key) }
+
+ before do
+ circuit_store.store(key, value)
+ end
+
+ it_behaves_like 'reliable circuit breaker store method'
+
+ it 'deletes key' do
+ delete
+
+ with_redis do |redis|
+ expect(redis.exists?(key)).to eq(false)
+ end
+ end
+ end
+
+ def with_redis(&block)
+ Gitlab::Redis::RateLimiting.with(&block)
+ end
+end
diff --git a/spec/lib/gitlab/circuit_breaker_spec.rb b/spec/lib/gitlab/circuit_breaker_spec.rb
new file mode 100644
index 0000000000000..4cd2f41869eee
--- /dev/null
+++ b/spec/lib/gitlab/circuit_breaker_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::CircuitBreaker, :clean_gitlab_redis_rate_limiting, feature_category: :shared do
+ let(:service_name) { 'DummyService' }
+ let(:volume_threshold) { 5 }
+ let(:circuit) do
+ Circuitbox.circuit(service_name,
+ { volume_threshold: volume_threshold, exceptions: [Gitlab::CircuitBreaker::InternalServerError] })
+ end
+
+ let(:dummy_class) do
+ Class.new do
+ def dummy_method
+ Gitlab::CircuitBreaker.run_with_circuit('DummyService') do
+ raise Gitlab::CircuitBreaker::InternalServerError
+ end
+ end
+
+ def another_dummy_method
+ Gitlab::CircuitBreaker.run_with_circuit('DummyService') do
+ # Do nothing but successful.
+ end
+ end
+ end
+ end
+
+ subject(:instance) { dummy_class.new }
+
+ before do
+ stub_const(service_name, dummy_class)
+ allow(Circuitbox).to receive(:circuit).and_return(circuit)
+ end
+
+ # rubocop: disable RSpec/AnyInstanceOf -- the instance is defined by an initializer
+ describe '#circuit' do
+ it 'returns nil value' do
+ expect(instance.dummy_method).to be_nil
+ end
+
+ it 'does not raise an error' do
+ expect { instance.dummy_method }.not_to raise_error
+ end
+
+ context 'when failed multiple times below volume threshold' do
+ it 'does not open the circuit' do
+ expect_any_instance_of(Gitlab::CircuitBreaker::Notifier).to receive(:notify)
+ .with(anything, 'failure')
+ .exactly(4).times
+
+ 4.times do
+ instance.dummy_method
+ end
+
+ expect(circuit).not_to be_open
+ end
+ end
+
+ context 'when failed multiple times over volume threshold' do
+ it 'allows the call 5 times, then opens the circuit and skips subsequent calls' do
+ expect_any_instance_of(Gitlab::CircuitBreaker::Notifier).to receive(:notify)
+ .with(anything, 'failure')
+ .exactly(5).times
+
+ expect_any_instance_of(Gitlab::CircuitBreaker::Notifier).to receive(:notify)
+ .with(anything, 'open')
+ .once
+
+ expect_any_instance_of(Gitlab::CircuitBreaker::Notifier).to receive(:notify)
+ .with(anything, 'skipped')
+ .once
+
+ 6.times do
+ instance.dummy_method
+ end
+
+ expect(circuit).to be_open
+ end
+ end
+
+ context 'when circuit is previously open' do
+ before do
+ # Opens the circuit
+ 6.times do
+ instance.dummy_method
+ end
+
+ # Deletes the open key
+ circuit.try_close_next_time
+ end
+
+ context 'when does not fail again' do
+ it 'closes the circuit' do
+ instance.another_dummy_method
+
+ expect(circuit).not_to be_open
+ end
+ end
+
+ context 'when fails again' do
+ it 'opens the circuit' do
+ instance.dummy_method
+
+ expect(circuit).to be_open
+ end
+ end
+ end
+ end
+ # rubocop: enable RSpec/AnyInstanceOf
+
+ describe '#run_with_circuit' do
+ let(:block) { proc {} }
+
+ it 'runs the code block within the Circuitbox circuit' do
+ expect(circuit).to receive(:run).with(exception: false, &block)
+ described_class.run_with_circuit('service', &block)
+ end
+ end
+end
diff --git a/spec/lib/product_analytics/event_params_spec.rb b/spec/lib/product_analytics/event_params_spec.rb
deleted file mode 100644
index e560fd10dfdce..0000000000000
--- a/spec/lib/product_analytics/event_params_spec.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProductAnalytics::EventParams do
- describe '.parse_event_params' do
- subject { described_class.parse_event_params(raw_event) }
-
- let(:raw_event) { Gitlab::Json.parse(fixture_file('product_analytics/event.json')) }
-
- it 'extracts all params from raw event' do
- expected_params = {
- project_id: '1',
- platform: 'web',
- name_tracker: 'sp',
- v_tracker: 'js-2.14.0',
- event_id: 'fbf14096-74ee-47e4-883c-8a0d6cb72e37',
- domain_userid: '79543c31-cfc3-4479-a737-fafb9333c8ba',
- domain_sessionid: '54f6d3f3-f4f9-4fdc-87e0-a2c775234c1b',
- domain_sessionidx: 4,
- page_url: 'http://example.com/products/1',
- page_referrer: 'http://example.com/products/1',
- br_lang: 'en-US',
- br_cookies: true,
- os_timezone: 'America/Los_Angeles',
- doc_charset: 'UTF-8',
- se_category: 'category',
- se_action: 'action',
- se_label: 'label',
- se_property: 'property',
- se_value: 12.34
- }
-
- expect(subject).to include(expected_params)
- end
- end
-
- describe '.has_required_params?' do
- subject { described_class.has_required_params?(params) }
-
- context 'aid and eid are present' do
- let(:params) { { 'aid' => 1, 'eid' => 2 } }
-
- it { expect(subject).to be_truthy }
- end
-
- context 'aid and eid are missing' do
- let(:params) { {} }
-
- it { expect(subject).to be_falsey }
- end
-
- context 'eid is missing' do
- let(:params) { { 'aid' => 1 } }
-
- it { expect(subject).to be_falsey }
- end
- end
-end
diff --git a/spec/models/product_analytics_event_spec.rb b/spec/models/product_analytics_event_spec.rb
deleted file mode 100644
index 801e6dd5e10b6..0000000000000
--- a/spec/models/product_analytics_event_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-RSpec.describe ProductAnalyticsEvent, type: :model do
- it { is_expected.to belong_to(:project) }
- it { expect(described_class).to respond_to(:order_by_time) }
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:project_id) }
- it { is_expected.to validate_presence_of(:event_id) }
- it { is_expected.to validate_presence_of(:v_collector) }
- it { is_expected.to validate_presence_of(:v_etl) }
- end
-
- describe '.timerange' do
- let_it_be(:event_1) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 1.day) }
- let_it_be(:event_2) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 5.days) }
- let_it_be(:event_3) { create(:product_analytics_event, collector_tstamp: Time.zone.now - 15.days) }
-
- it { expect(described_class.timerange(3.days)).to match_array([event_1]) }
- it { expect(described_class.timerange(7.days)).to match_array([event_1, event_2]) }
- it { expect(described_class.timerange(30.days)).to match_array([event_1, event_2, event_3]) }
- end
-
- describe '.count_by_graph' do
- let_it_be(:events) do
- [
- create(:product_analytics_event, platform: 'web'),
- create(:product_analytics_event, platform: 'web'),
- create(:product_analytics_event, platform: 'app'),
- create(:product_analytics_event, platform: 'mobile', collector_tstamp: Time.zone.now - 10.days)
- ]
- end
-
- it { expect(described_class.count_by_graph('platform', 7.days)).to eq({ 'app' => 1, 'web' => 2 }) }
- it { expect(described_class.count_by_graph('platform', 30.days)).to eq({ 'app' => 1, 'mobile' => 1, 'web' => 2 }) }
- end
-
- describe '.count_collector_tstamp_by_day' do
- let_it_be(:time_now) { Time.zone.now }
- let_it_be(:time_ago) { Time.zone.now - 5.days }
-
- let_it_be(:events) do
- create_list(:product_analytics_event, 3, collector_tstamp: time_now) +
- create_list(:product_analytics_event, 2, collector_tstamp: time_ago)
- end
-
- subject { described_class.count_collector_tstamp_by_day(7.days) }
-
- it { is_expected.to eq({ time_now.beginning_of_day => 3, time_ago.beginning_of_day => 2 }) }
- end
-end
diff --git a/spec/services/product_analytics/build_activity_graph_service_spec.rb b/spec/services/product_analytics/build_activity_graph_service_spec.rb
deleted file mode 100644
index 2eb35523da779..0000000000000
--- a/spec/services/product_analytics/build_activity_graph_service_spec.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProductAnalytics::BuildActivityGraphService, feature_category: :product_analytics_data_management do
- let_it_be(:project) { create(:project) }
- let_it_be(:time_now) { Time.zone.now }
- let_it_be(:time_ago) { Time.zone.now - 5.days }
-
- let_it_be(:events) do
- [
- create(:product_analytics_event, project: project, collector_tstamp: time_now),
- create(:product_analytics_event, project: project, collector_tstamp: time_now),
- create(:product_analytics_event, project: project, collector_tstamp: time_now),
- create(:product_analytics_event, project: project, collector_tstamp: time_ago),
- create(:product_analytics_event, project: project, collector_tstamp: time_ago)
- ]
- end
-
- let(:params) { { timerange: 7 } }
-
- subject { described_class.new(project, params).execute }
-
- it 'returns a valid graph hash' do
- expected_hash = {
- id: 'collector_tstamp',
- keys: [time_ago.to_date, time_now.to_date],
- values: [2, 3]
- }
-
- expect(subject).to eq(expected_hash)
- end
-end
diff --git a/spec/services/product_analytics/build_graph_service_spec.rb b/spec/services/product_analytics/build_graph_service_spec.rb
deleted file mode 100644
index a850d69e53cd9..0000000000000
--- a/spec/services/product_analytics/build_graph_service_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ProductAnalytics::BuildGraphService, feature_category: :product_analytics_data_management do
- let_it_be(:project) { create(:project) }
-
- let_it_be(:events) do
- [
- create(:product_analytics_event, project: project, platform: 'web'),
- create(:product_analytics_event, project: project, platform: 'web'),
- create(:product_analytics_event, project: project, platform: 'app'),
- create(:product_analytics_event, project: project, platform: 'mobile'),
- create(:product_analytics_event, project: project, platform: 'mobile', collector_tstamp: Time.zone.now - 60.days)
- ]
- end
-
- let(:params) { { graph: 'platform', timerange: 5 } }
-
- subject { described_class.new(project, params).execute }
-
- it 'returns a valid graph hash' do
- expect(subject[:id]).to eq(:platform)
- expect(subject[:keys]).to eq(%w[app mobile web])
- expect(subject[:values]).to eq([1, 1, 2])
- end
-end
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 0b09bb353d553..90229f54dec42 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -2185,6 +2185,36 @@
expect(message).to eq('Submitted the current review.')
end
end
+
+ context 'when parameters are passed' do
+ context 'with approve parameter' do
+ it 'calls MergeRequests::ApprovalService service' do
+ expect_next_instance_of(
+ MergeRequests::ApprovalService, project: merge_request.project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ _, _, message = service.execute('/submit_review approve', merge_request)
+
+ expect(message).to eq('Submitted the current review.')
+ end
+ end
+
+ context 'with review state parameter' do
+ it 'calls MergeRequests::UpdateReviewerStateService service' do
+ expect_next_instance_of(
+ MergeRequests::UpdateReviewerStateService, project: merge_request.project, current_user: current_user
+ ) do |service|
+ expect(service).to receive(:execute).with(merge_request, 'requested_changes')
+ end
+
+ _, _, message = service.execute('/submit_review requested_changes', merge_request)
+
+ expect(message).to eq('Submitted the current review.')
+ end
+ end
+ end
end
context 'request_changes command' do
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 17dc7fa6dbf81..13858360a6c3b 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -6920,7 +6920,6 @@
- './spec/lib/peek/views/external_http_spec.rb'
- './spec/lib/peek/views/memory_spec.rb'
- './spec/lib/peek/views/redis_detailed_spec.rb'
-- './spec/lib/product_analytics/event_params_spec.rb'
- './spec/lib/prometheus/cleanup_multiproc_dir_service_spec.rb'
- './spec/lib/prometheus/pid_provider_spec.rb'
- './spec/lib/quality/seeders/issues_spec.rb'
@@ -7510,7 +7509,6 @@
- './spec/models/preloaders/merge_request_diff_preloader_spec.rb'
- './spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb'
- './spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb'
-- './spec/models/product_analytics_event_spec.rb'
- './spec/models/programming_language_spec.rb'
- './spec/models/project_authorization_spec.rb'
- './spec/models/project_auto_devops_spec.rb'
@@ -8989,8 +8987,6 @@
- './spec/services/personal_access_tokens/revoke_service_spec.rb'
- './spec/services/post_receive_service_spec.rb'
- './spec/services/preview_markdown_service_spec.rb'
-- './spec/services/product_analytics/build_activity_graph_service_spec.rb'
-- './spec/services/product_analytics/build_graph_service_spec.rb'
- './spec/services/projects/after_rename_service_spec.rb'
- './spec/services/projects/alerting/notify_service_spec.rb'
- './spec/services/projects/all_issues_count_service_spec.rb'
diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb
index 0ac5efa2e6d4a..6cfc85287cd6d 100644
--- a/spec/views/projects/tags/index.html.haml_spec.rb
+++ b/spec/views/projects/tags/index.html.haml_spec.rb
@@ -92,13 +92,12 @@
render
expect(page.find('.tags .content-list li', text: tag)).to have_css '[data-testid="status_success_borderless-icon"]'
- expect(page.all('.tags .content-list li')).to all(have_css('svg.s16'))
end
it 'shows no build status or placeholder when no pipelines present' do
render
- expect(page.all('.tags .content-list li')).not_to have_css 'svg.s16'
+ expect(page.find('.tags .content-list li', text: tag)).not_to have_css '[data-testid="status_success_borderless-icon"]'
end
it 'shows no build status or placeholder when pipelines are private' do
@@ -107,7 +106,7 @@
render
- expect(page.all('.tags .content-list li')).not_to have_css 'svg.s16'
+ expect(page.find('.tags .content-list li', text: tag)).not_to have_css '[data-testid="status_success_borderless-icon"]'
end
end